@ldclabs/kip-lang 0.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +89 -64
  4. package/dist/ast.d.ts +511 -145
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/diagnostics.d.ts +8 -2
  7. package/dist/diagnostics.d.ts.map +1 -1
  8. package/dist/diagnostics.js +32 -3
  9. package/dist/diagnostics.js.map +1 -1
  10. package/dist/exec-ast.d.ts +514 -149
  11. package/dist/exec-ast.d.ts.map +1 -1
  12. package/dist/exec-ast.js +8 -7
  13. package/dist/exec-ast.js.map +1 -1
  14. package/dist/formatter.d.ts.map +1 -1
  15. package/dist/formatter.js +870 -479
  16. package/dist/formatter.js.map +1 -1
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lexer.d.ts.map +1 -1
  22. package/dist/lexer.js +12 -30
  23. package/dist/lexer.js.map +1 -1
  24. package/dist/lower.d.ts +1 -2
  25. package/dist/lower.d.ts.map +1 -1
  26. package/dist/lower.js +1355 -594
  27. package/dist/lower.js.map +1 -1
  28. package/dist/parser.d.ts.map +1 -1
  29. package/dist/parser.js +2410 -1300
  30. package/dist/parser.js.map +1 -1
  31. package/dist/semantics.d.ts +11 -7
  32. package/dist/semantics.d.ts.map +1 -1
  33. package/dist/semantics.js +295 -180
  34. package/dist/semantics.js.map +1 -1
  35. package/dist/token.d.ts +130 -40
  36. package/dist/token.d.ts.map +1 -1
  37. package/dist/token.js +264 -83
  38. package/dist/token.js.map +1 -1
  39. package/dist/version.d.ts +2 -2
  40. package/dist/version.js +2 -2
  41. package/package.json +35 -5
  42. package/src/ast.ts +914 -0
  43. package/src/budget.ts +108 -0
  44. package/src/diagnostics.ts +182 -0
  45. package/src/errors.ts +42 -0
  46. package/src/exec-ast.ts +614 -0
  47. package/src/formatter.ts +1339 -0
  48. package/src/index.ts +226 -0
  49. package/src/lexer.ts +459 -0
  50. package/src/lower.ts +2094 -0
  51. package/src/parser.ts +3506 -0
  52. package/src/semantics.ts +392 -0
  53. package/src/token.ts +408 -0
  54. package/src/version.ts +13 -0
package/src/parser.ts ADDED
@@ -0,0 +1,3506 @@
1
+ import { tokenize } from './lexer.js'
2
+ import {
3
+ Token,
4
+ TokenType,
5
+ isTrivia,
6
+ isIdentifierLike,
7
+ isAggregate
8
+ } from './token.js'
9
+ import type { Range, Position } from './token.js'
10
+ import type {
11
+ Program,
12
+ Statement,
13
+ MutationClause,
14
+ FindStatement,
15
+ AsOfClause,
16
+ ForTimeClause,
17
+ EpistemicClause,
18
+ OrderByClause,
19
+ OrderItem,
20
+ LimitClause,
21
+ CursorClause,
22
+ WhereClause,
23
+ WherePattern,
24
+ ConceptPattern,
25
+ PropositionPattern,
26
+ AssertionPattern,
27
+ EvidencePattern,
28
+ ActivityPattern,
29
+ StructuralPattern,
30
+ BeliefPattern,
31
+ BeliefSlotPattern,
32
+ FilterClause,
33
+ NotClause,
34
+ OptionalClause,
35
+ UnionClause,
36
+ PropositionTuple,
37
+ Term,
38
+ PredicateAtom,
39
+ RawPredicateExpression,
40
+ PredicatePathAtom,
41
+ PathQuantifier,
42
+ ObjectPattern,
43
+ MutateStatement,
44
+ CreateConceptStatement,
45
+ UpsertConceptStatement,
46
+ EnsurePropositionStatement,
47
+ AssertStatement,
48
+ CreateEvidenceStatement,
49
+ CreateAssertionStatement,
50
+ CreateActivityStatement,
51
+ TypeClause,
52
+ ClientKeyClause,
53
+ NameClause,
54
+ MatchClause,
55
+ SetFieldsClause,
56
+ SetAttributesClause,
57
+ SetFacetClause,
58
+ UnsetAttributesClause,
59
+ UnsetFacetClause,
60
+ UnsetField,
61
+ SetStructuralClause,
62
+ StructuralAssignment,
63
+ UnsetStructuralClause,
64
+ StructuralRemoval,
65
+ ExpectVersionClause,
66
+ ExpectStateClause,
67
+ UpdateStatement,
68
+ UpdateAction,
69
+ RetractAssertionStatement,
70
+ SupersedeAssertionStatement,
71
+ CorrectEvidenceStatement,
72
+ TransitionActivityStatement,
73
+ SetRetentionStatement,
74
+ ArchiveStatement,
75
+ TombstoneStatement,
76
+ PurgeStatement,
77
+ MergeConceptStatement,
78
+ DescribeStatement,
79
+ DescribeTargetKind,
80
+ ListStatement,
81
+ ListTargetKind,
82
+ SearchStatement,
83
+ SearchKind,
84
+ VerifyStatement,
85
+ VerifyTargetKind,
86
+ ValidateStatement,
87
+ ValidateTargetKind,
88
+ PreviewStatement,
89
+ HistoryStatement,
90
+ ChangesStatement,
91
+ SnapshotStatement,
92
+ ExportCapsuleStatement,
93
+ Expression,
94
+ FunctionCallExpr,
95
+ AggregateExpr,
96
+ FieldAccess,
97
+ FieldStep,
98
+ VariableRef,
99
+ ParameterRef,
100
+ StringLiteral,
101
+ NumberLiteral,
102
+ BooleanLiteral,
103
+ NullLiteral,
104
+ ScalarValue,
105
+ SchemaSymbol,
106
+ TargetRef,
107
+ ArrayLiteral,
108
+ ObjectLiteral,
109
+ ObjectEntry
110
+ } from './ast.js'
111
+ import type { Diagnostic } from './diagnostics.js'
112
+
113
+ export interface ParseResult {
114
+ ast: Program
115
+ diagnostics: Diagnostic[]
116
+ }
117
+
118
+ /**
119
+ * Which grammar owns the WHERE block being parsed.
120
+ *
121
+ * KQL owns the two reviewed divergences: `proposition_tuple` accepts raw
122
+ * predicate path expressions, and `where_clause` additionally accepts
123
+ * BELIEF / BELIEF SLOT. KML and META EXPORT get neither — a virtual
124
+ * Projection can never be a mutation target or an export selector.
125
+ */
126
+ type Dialect = 'kql' | 'raw'
127
+
128
+ /** The four baseline scalar types; arrays and objects are not Core Literals. */
129
+ type LiteralNode =
130
+ | StringLiteral
131
+ | NumberLiteral
132
+ | BooleanLiteral
133
+ | NullLiteral
134
+
135
+ export function parse(source: string): ParseResult {
136
+ const allTokens = tokenize(source)
137
+ const parser = new Parser(allTokens, source)
138
+ return parser.parse()
139
+ }
140
+
141
+ class Parser {
142
+ private tokens: Token[]
143
+ private pos: number = 0
144
+ private diagnostics: Diagnostic[] = []
145
+ private source: string
146
+ private dialect: Dialect = 'kql'
147
+
148
+ constructor(tokens: Token[], source: string) {
149
+ // Filter out trivia for parsing, but keep comments for attachment later
150
+ this.tokens = tokens.filter(
151
+ (t) => !isTrivia(t.type) || t.type === TokenType.Comment
152
+ )
153
+ this.source = source
154
+ }
155
+
156
+ parse(): ParseResult {
157
+ const statements: Statement[] = []
158
+ const start = this.currentPos()
159
+ this.skipComments()
160
+ while (!this.isAtEnd()) {
161
+ const before = this.pos
162
+ this.skipComments()
163
+ if (this.isAtEnd()) break
164
+ try {
165
+ const stmt = this.parseStatement()
166
+ if (stmt) statements.push(stmt)
167
+ } catch {
168
+ // Error recovery: skip to next statement-level keyword
169
+ this.recoverToNextStatement()
170
+ }
171
+ // A sub-parser that rejects its first token reports and returns
172
+ // without consuming it, so a loop keyed on that token would spin
173
+ // forever building diagnostics. Stop as soon as nothing moved.
174
+ if (this.pos === before) break
175
+ }
176
+ const end = this.currentPos()
177
+ return {
178
+ ast: { kind: 'Program', statements, range: { start, end } },
179
+ diagnostics: this.diagnostics
180
+ }
181
+ }
182
+
183
+ // ────────────────────────────────────────────────────────────────────
184
+ // Statement dispatch
185
+ // ────────────────────────────────────────────────────────────────────
186
+
187
+ private parseStatement(): Statement | null {
188
+ const tok = this.current()
189
+ switch (tok.type) {
190
+ // KQL
191
+ case TokenType.Find:
192
+ return this.parseFindStatement()
193
+
194
+ // KML
195
+ case TokenType.Mutate:
196
+ return this.parseMutateStatement()
197
+ case TokenType.Create:
198
+ case TokenType.Upsert:
199
+ case TokenType.Ensure:
200
+ case TokenType.Assert:
201
+ case TokenType.Update:
202
+ case TokenType.Retract:
203
+ case TokenType.Supersede:
204
+ case TokenType.Correct:
205
+ case TokenType.Transition:
206
+ case TokenType.Set:
207
+ case TokenType.Archive:
208
+ case TokenType.Tombstone:
209
+ case TokenType.Purge:
210
+ case TokenType.Merge:
211
+ return this.parseMutationClause()
212
+
213
+ // META
214
+ case TokenType.Describe:
215
+ return this.parseDescribeStatement()
216
+ case TokenType.List:
217
+ return this.parseListStatement()
218
+ case TokenType.Search:
219
+ return this.parseSearchStatement()
220
+ case TokenType.Verify:
221
+ return this.parseVerifyStatement()
222
+ case TokenType.Validate:
223
+ return this.parseValidateStatement()
224
+ case TokenType.Preview:
225
+ return this.parsePreviewStatement()
226
+ case TokenType.History:
227
+ return this.parseHistoryStatement()
228
+ case TokenType.Changes:
229
+ return this.parseChangesStatement()
230
+ case TokenType.Snapshot:
231
+ return this.parseSnapshotStatement()
232
+ case TokenType.Export:
233
+ return this.parseExportCapsuleStatement()
234
+
235
+ default:
236
+ this.error(
237
+ `Unexpected token '${tok.value}': expected a KQL, KML or META statement`,
238
+ tok
239
+ )
240
+ return null
241
+ }
242
+ }
243
+
244
+ /** Every mutation legal at statement level and inside `MUTATE { ... }`. */
245
+ private parseMutationClause(): MutationClause {
246
+ const tok = this.current()
247
+ switch (tok.type) {
248
+ case TokenType.Create:
249
+ return this.parseCreateStatement()
250
+ case TokenType.Upsert:
251
+ return this.parseUpsertConcept()
252
+ case TokenType.Ensure:
253
+ return this.parseEnsureProposition()
254
+ case TokenType.Assert:
255
+ return this.parseAssertStatement()
256
+ case TokenType.Update:
257
+ return this.parseUpdateStatement()
258
+ case TokenType.Retract:
259
+ return this.parseRetractAssertion()
260
+ case TokenType.Supersede:
261
+ return this.parseSupersedeAssertion()
262
+ case TokenType.Correct:
263
+ return this.parseCorrectEvidence()
264
+ case TokenType.Transition:
265
+ return this.parseTransitionActivity()
266
+ case TokenType.Set:
267
+ return this.parseSetRetention()
268
+ case TokenType.Archive:
269
+ return this.parseArchiveStatement()
270
+ case TokenType.Tombstone:
271
+ return this.parseTombstoneStatement()
272
+ case TokenType.Purge:
273
+ return this.parsePurgeStatement()
274
+ case TokenType.Merge:
275
+ return this.parseMergeConcept()
276
+ default:
277
+ this.error(`Unexpected token '${tok.value}': expected a KML mutation`, tok)
278
+ throw new ParseAbort()
279
+ }
280
+ }
281
+
282
+ // ────────────────────────────────────────────────────────────────────
283
+ // KQL — FIND
284
+ // ────────────────────────────────────────────────────────────────────
285
+
286
+ private parseFindStatement(): FindStatement {
287
+ const leadingComments = this.collectLeadingComments()
288
+ const start = this.currentPos()
289
+ this.dialect = 'kql'
290
+ this.expect(TokenType.Find)
291
+ this.expect(TokenType.LParen)
292
+
293
+ const projections: Expression[] = []
294
+ if (!this.check(TokenType.RParen)) {
295
+ do {
296
+ projections.push(this.parseProjectionExpression())
297
+ } while (this.match(TokenType.Comma))
298
+ }
299
+ if (projections.length === 0) {
300
+ this.error('FIND requires at least one projection', this.current())
301
+ }
302
+ this.expect(TokenType.RParen)
303
+
304
+ this.expectKeywordWithSpace(TokenType.Where)
305
+ const where = this.parseWhereClause()
306
+
307
+ let asOf: AsOfClause | undefined
308
+ let forTime: ForTimeClause | undefined
309
+ let epistemic: EpistemicClause | undefined
310
+ let orderBy: OrderByClause | undefined
311
+ let limit: LimitClause | undefined
312
+ let cursor: CursorClause | undefined
313
+ let lastClauseOrder = -1
314
+
315
+ // The grammar fixes this order. Accepting any order here would let a
316
+ // command run on this parser that a conformant engine rejects, so each
317
+ // clause is taken once and out-of-order repeats are reported.
318
+ for (;;) {
319
+ const tok = this.current()
320
+ if (this.check(TokenType.As)) {
321
+ lastClauseOrder = this.checkClauseOrder(0, lastClauseOrder, 'AS OF', tok)
322
+ this.rejectRepeat(asOf, 'AS OF', tok)
323
+ asOf = this.parseAsOfClause()
324
+ } else if (this.check(TokenType.For)) {
325
+ lastClauseOrder = this.checkClauseOrder(
326
+ 1,
327
+ lastClauseOrder,
328
+ 'FOR TIME',
329
+ tok
330
+ )
331
+ this.rejectRepeat(forTime, 'FOR TIME', tok)
332
+ forTime = this.parseForTimeClause()
333
+ } else if (this.check(TokenType.With)) {
334
+ lastClauseOrder = this.checkClauseOrder(
335
+ 2,
336
+ lastClauseOrder,
337
+ 'WITH EPISTEMIC',
338
+ tok
339
+ )
340
+ this.rejectRepeat(epistemic, 'WITH EPISTEMIC', tok)
341
+ epistemic = this.parseEpistemicClause()
342
+ } else if (this.check(TokenType.Order)) {
343
+ lastClauseOrder = this.checkClauseOrder(
344
+ 3,
345
+ lastClauseOrder,
346
+ 'ORDER BY',
347
+ tok
348
+ )
349
+ this.rejectRepeat(orderBy, 'ORDER BY', tok)
350
+ orderBy = this.parseOrderBy()
351
+ } else if (this.check(TokenType.Limit)) {
352
+ lastClauseOrder = this.checkClauseOrder(4, lastClauseOrder, 'LIMIT', tok)
353
+ this.rejectRepeat(limit, 'LIMIT', tok)
354
+ limit = this.parseLimitClause()
355
+ } else if (this.check(TokenType.Cursor)) {
356
+ lastClauseOrder = this.checkClauseOrder(5, lastClauseOrder, 'CURSOR', tok)
357
+ this.rejectRepeat(cursor, 'CURSOR', tok)
358
+ cursor = this.parseCursorClause()
359
+ } else {
360
+ break
361
+ }
362
+ }
363
+
364
+ return {
365
+ kind: 'FindStatement',
366
+ projections,
367
+ where,
368
+ asOf,
369
+ forTime,
370
+ epistemic,
371
+ orderBy,
372
+ limit,
373
+ cursor,
374
+ range: { start, end: this.endPos() },
375
+ leadingComments: leadingComments.length ? leadingComments : undefined
376
+ }
377
+ }
378
+
379
+ /** `projection_expression = aggregate_expression | expression` */
380
+ private parseProjectionExpression(): Expression {
381
+ return this.parseExpression()
382
+ }
383
+
384
+ private parseAsOfClause(): AsOfClause {
385
+ const start = this.currentPos()
386
+ const first = this.expect(TokenType.As)
387
+ this.expectSecondWord(TokenType.Of, first)
388
+
389
+ let basis: 'SEQ' | 'TX' | 'TIME'
390
+ if (this.match(TokenType.Seq)) {
391
+ basis = 'SEQ'
392
+ } else if (this.match(TokenType.Tx)) {
393
+ basis = 'TX'
394
+ } else if (this.match(TokenType.Time)) {
395
+ basis = 'TIME'
396
+ } else {
397
+ this.error(
398
+ `Expected SEQ, TX or TIME after AS OF but got '${this.current().value}'`,
399
+ this.current()
400
+ )
401
+ basis = 'SEQ'
402
+ }
403
+ const value = this.parseScalarValue()
404
+ return {
405
+ kind: 'AsOfClause',
406
+ basis,
407
+ value,
408
+ range: { start, end: this.endPos() }
409
+ }
410
+ }
411
+
412
+ private parseForTimeClause(): ForTimeClause {
413
+ const start = this.currentPos()
414
+ const first = this.expect(TokenType.For)
415
+ this.expectSecondWord(TokenType.Time, first)
416
+ const value = this.parseScalarValue()
417
+ return {
418
+ kind: 'ForTimeClause',
419
+ value,
420
+ range: { start, end: this.endPos() }
421
+ }
422
+ }
423
+
424
+ private parseEpistemicClause(): EpistemicClause {
425
+ const start = this.currentPos()
426
+ const first = this.expect(TokenType.With)
427
+ this.expectSecondWord(TokenType.Epistemic, first)
428
+ const options = this.parseObjectLiteral()
429
+ return {
430
+ kind: 'EpistemicClause',
431
+ options,
432
+ range: { start, end: this.endPos() }
433
+ }
434
+ }
435
+
436
+ private parseOrderBy(): OrderByClause {
437
+ const start = this.currentPos()
438
+ const first = this.expect(TokenType.Order)
439
+ this.expectSecondWord(TokenType.By, first)
440
+
441
+ const items: OrderItem[] = []
442
+ do {
443
+ items.push(this.parseOrderItem())
444
+ } while (this.match(TokenType.Comma))
445
+
446
+ return {
447
+ kind: 'OrderByClause',
448
+ items,
449
+ range: { start, end: this.endPos() }
450
+ }
451
+ }
452
+
453
+ private parseOrderItem(): OrderItem {
454
+ const start = this.currentPos()
455
+ const expression = this.parseProjectionExpression()
456
+ let direction: 'ASC' | 'DESC' | undefined
457
+ if (this.match(TokenType.Asc)) direction = 'ASC'
458
+ else if (this.match(TokenType.Desc)) direction = 'DESC'
459
+ return {
460
+ kind: 'OrderItem',
461
+ expression,
462
+ direction,
463
+ range: { start, end: this.endPos() }
464
+ }
465
+ }
466
+
467
+ private parseLimitClause(): LimitClause {
468
+ const start = this.currentPos()
469
+ this.expect(TokenType.Limit)
470
+ const value = this.parseScalarValue()
471
+ return {
472
+ kind: 'LimitClause',
473
+ value,
474
+ range: { start, end: this.endPos() }
475
+ }
476
+ }
477
+
478
+ private parseCursorClause(): CursorClause {
479
+ const start = this.currentPos()
480
+ this.expect(TokenType.Cursor)
481
+ const value = this.parseScalarValue()
482
+ return {
483
+ kind: 'CursorClause',
484
+ value,
485
+ range: { start, end: this.endPos() }
486
+ }
487
+ }
488
+
489
+ // ────────────────────────────────────────────────────────────────────
490
+ // WHERE
491
+ // ────────────────────────────────────────────────────────────────────
492
+
493
+ private parseWhereClause(): WhereClause {
494
+ const start = this.currentPos()
495
+ this.expect(TokenType.LBrace)
496
+ const patterns = this.parseWherePatterns()
497
+ this.expect(TokenType.RBrace)
498
+ return {
499
+ kind: 'WhereClause',
500
+ patterns,
501
+ range: { start, end: this.endPos() }
502
+ }
503
+ }
504
+
505
+ private parseWherePatterns(): WherePattern[] {
506
+ const patterns: WherePattern[] = []
507
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
508
+ const before = this.pos
509
+ const pattern = this.parseWherePattern()
510
+ if (pattern) patterns.push(pattern)
511
+ if (this.pos === before) break
512
+ // WHERE items are whitespace-delimited; a comma between them is not
513
+ // grammar, so report it rather than silently accepting both spellings.
514
+ if (this.check(TokenType.Comma)) {
515
+ this.error(
516
+ 'WHERE items are separated by whitespace, not commas',
517
+ this.current()
518
+ )
519
+ this.advance()
520
+ }
521
+ }
522
+ return patterns
523
+ }
524
+
525
+ private parseWherePattern(): WherePattern | null {
526
+ const tok = this.current()
527
+ switch (tok.type) {
528
+ case TokenType.Variable:
529
+ return this.parseVariableLeadingPattern()
530
+ case TokenType.LParen:
531
+ case TokenType.Proposition:
532
+ return this.parsePropositionPattern(undefined)
533
+ case TokenType.Structural:
534
+ return this.parseStructuralPattern(undefined)
535
+ case TokenType.Filter:
536
+ return this.parseFilterClause()
537
+ case TokenType.Not:
538
+ return this.parseNotClause()
539
+ case TokenType.Optional:
540
+ return this.parseOptionalClause()
541
+ case TokenType.Union:
542
+ return this.parseUnionClause()
543
+ default:
544
+ this.error(`Unexpected token '${tok.value}' in WHERE block`, tok)
545
+ this.advance()
546
+ return null
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Disambiguates the pattern families that all begin with a variable.
552
+ *
553
+ * `?x {`/`?x CONCEPT {` is a Concept, `?x (`/`?x PROPOSITION (` a raw
554
+ * Proposition, and the remaining families name their kind outright.
555
+ */
556
+ private parseVariableLeadingPattern(): WherePattern {
557
+ const variable = this.parseVariableRef()
558
+ const tok = this.current()
559
+
560
+ switch (tok.type) {
561
+ case TokenType.LBrace:
562
+ case TokenType.Concept:
563
+ return this.parseConceptPattern(variable)
564
+ case TokenType.LParen:
565
+ case TokenType.Proposition:
566
+ return this.parsePropositionPattern(variable)
567
+ case TokenType.Assertion:
568
+ return this.parseAssertionPattern(variable)
569
+ case TokenType.Evidence:
570
+ return this.parseEvidencePattern(variable)
571
+ case TokenType.Activity:
572
+ return this.parseActivityPattern(variable)
573
+ case TokenType.Structural:
574
+ return this.parseStructuralPattern(variable)
575
+ case TokenType.Belief:
576
+ return this.parseBeliefPattern(variable)
577
+ default:
578
+ this.error(
579
+ `Expected a pattern body after ${variable.name} but got '${tok.value}'`,
580
+ tok
581
+ )
582
+ throw new ParseAbort()
583
+ }
584
+ }
585
+
586
+ private parseConceptPattern(variable: VariableRef): ConceptPattern {
587
+ const explicit = this.match(TokenType.Concept)
588
+ const matcher = this.parseObjectPattern()
589
+ return {
590
+ kind: 'ConceptPattern',
591
+ variable,
592
+ explicit,
593
+ matcher,
594
+ range: { start: variable.range.start, end: this.endPos() }
595
+ }
596
+ }
597
+
598
+ private parsePropositionPattern(
599
+ variable: VariableRef | undefined
600
+ ): PropositionPattern {
601
+ const start = variable ? variable.range.start : this.currentPos()
602
+ const explicit = this.match(TokenType.Proposition)
603
+ const tuple = this.parsePropositionTuple()
604
+ return {
605
+ kind: 'PropositionPattern',
606
+ variable,
607
+ explicit,
608
+ tuple,
609
+ range: { start, end: this.endPos() }
610
+ }
611
+ }
612
+
613
+ private parseAssertionPattern(variable: VariableRef): AssertionPattern {
614
+ this.expect(TokenType.Assertion)
615
+ const matcher = this.parseObjectPattern()
616
+ return {
617
+ kind: 'AssertionPattern',
618
+ variable,
619
+ matcher,
620
+ range: { start: variable.range.start, end: this.endPos() }
621
+ }
622
+ }
623
+
624
+ private parseEvidencePattern(variable: VariableRef): EvidencePattern {
625
+ this.expect(TokenType.Evidence)
626
+ const matcher = this.parseObjectPattern()
627
+ return {
628
+ kind: 'EvidencePattern',
629
+ variable,
630
+ matcher,
631
+ range: { start: variable.range.start, end: this.endPos() }
632
+ }
633
+ }
634
+
635
+ private parseActivityPattern(variable: VariableRef): ActivityPattern {
636
+ this.expect(TokenType.Activity)
637
+ const matcher = this.parseObjectPattern()
638
+ return {
639
+ kind: 'ActivityPattern',
640
+ variable,
641
+ matcher,
642
+ range: { start: variable.range.start, end: this.endPos() }
643
+ }
644
+ }
645
+
646
+ private parseStructuralPattern(
647
+ variable: VariableRef | undefined
648
+ ): StructuralPattern {
649
+ const start = variable ? variable.range.start : this.currentPos()
650
+ this.expect(TokenType.Structural)
651
+ this.expect(TokenType.LParen)
652
+ const subject = this.parseTerm()
653
+ this.expect(TokenType.Comma)
654
+ const field = this.parseSchemaSymbol()
655
+ this.expect(TokenType.Comma)
656
+ const object = this.parseTerm()
657
+ this.expect(TokenType.RParen)
658
+ return {
659
+ kind: 'StructuralPattern',
660
+ variable,
661
+ subject,
662
+ field,
663
+ object,
664
+ range: { start, end: this.endPos() }
665
+ }
666
+ }
667
+
668
+ private parseBeliefPattern(
669
+ variable: VariableRef
670
+ ): BeliefPattern | BeliefSlotPattern {
671
+ const start = variable.range.start
672
+ const beliefTok = this.expect(TokenType.Belief)
673
+
674
+ // `BELIEF SLOT (...)` — the whole functional slot, not one tuple.
675
+ if (this.check(TokenType.Slot)) {
676
+ this.expectSecondWord(TokenType.Slot, beliefTok)
677
+ this.expect(TokenType.LParen)
678
+ const subject = this.parseTerm()
679
+ this.expect(TokenType.Comma)
680
+ const predicate = this.parsePredicateAtom()
681
+ this.expect(TokenType.RParen)
682
+ this.rejectBeliefInRawDialect(start)
683
+ return {
684
+ kind: 'BeliefSlotPattern',
685
+ variable,
686
+ subject,
687
+ predicate,
688
+ range: { start, end: this.endPos() }
689
+ }
690
+ }
691
+
692
+ this.expect(TokenType.LParen)
693
+
694
+ // `BELIEF (?p)` projects an already-bound Proposition; `BELIEF (s, p, o)`
695
+ // projects a tuple. Only a lone variable followed by `)` is the former.
696
+ if (this.check(TokenType.Variable) && this.peekPast(1)?.type === TokenType.RParen) {
697
+ const proposition = this.parseVariableRef()
698
+ this.expect(TokenType.RParen)
699
+ this.rejectBeliefInRawDialect(start)
700
+ return {
701
+ kind: 'BeliefPattern',
702
+ variable,
703
+ proposition,
704
+ range: { start, end: this.endPos() }
705
+ }
706
+ }
707
+
708
+ // `BELIEF (id: ...)` — the operand is the Proposition expression slot, so
709
+ // the same id form that names a Proposition in a pattern names it here
710
+ // (Spec §43.2 / §46.1). Same recognition rule as parsePropositionTuple.
711
+ if (this.isPropositionIdStart()) {
712
+ this.advance() // id
713
+ this.advance() // :
714
+ const propositionId = this.parseScalarValue()
715
+ this.expect(TokenType.RParen)
716
+ this.rejectBeliefInRawDialect(start)
717
+ return {
718
+ kind: 'BeliefPattern',
719
+ variable,
720
+ propositionId,
721
+ range: { start, end: this.endPos() }
722
+ }
723
+ }
724
+
725
+ // `BELIEF (:p)` is the one spelling a reader might reach for that means
726
+ // nothing: a lone parameter is not a bound variable and not an id
727
+ // reference. Say what the reference form is instead of "expected ','",
728
+ // and recover as if the id form had been written so nothing cascades.
729
+ if (this.check(TokenType.Parameter) && this.peekPast(1)?.type === TokenType.RParen) {
730
+ const param = this.current()
731
+ this.error(
732
+ `BELIEF (${param.value}) is not a form: name the Proposition by (id: ${param.value}), or bind it first and write BELIEF (?p)`,
733
+ param
734
+ )
735
+ const propositionId = this.parseParameterRef()
736
+ this.expect(TokenType.RParen)
737
+ this.rejectBeliefInRawDialect(start)
738
+ return {
739
+ kind: 'BeliefPattern',
740
+ variable,
741
+ propositionId,
742
+ range: { start, end: this.endPos() }
743
+ }
744
+ }
745
+
746
+ const subject = this.parseTerm()
747
+ this.expect(TokenType.Comma)
748
+ const predicate = this.parsePredicateAtom()
749
+ this.expect(TokenType.Comma)
750
+ const object = this.parseTerm()
751
+ this.expect(TokenType.RParen)
752
+ this.rejectBeliefInRawDialect(start)
753
+ return {
754
+ kind: 'BeliefPattern',
755
+ variable,
756
+ subject,
757
+ predicate,
758
+ object,
759
+ range: { start, end: this.endPos() }
760
+ }
761
+ }
762
+
763
+ /**
764
+ * BELIEF is an Epistemic Projection: virtual, read-only, and derived from a
765
+ * policy. KML excludes it because a Projection can never be a mutation
766
+ * target; EXPORT excludes it because a capsule carries records, not
767
+ * interpretations.
768
+ */
769
+ private rejectBeliefInRawDialect(start: Position): void {
770
+ if (this.dialect === 'raw') {
771
+ this.diagnostics.push({
772
+ range: { start, end: this.endPos() },
773
+ severity: 'error',
774
+ message:
775
+ 'BELIEF is a read-only Epistemic Projection and cannot appear in a mutation or export selection',
776
+ code: 'KIP_1001'
777
+ })
778
+ }
779
+ }
780
+
781
+ private parseFilterClause(): FilterClause {
782
+ const start = this.currentPos()
783
+ this.expect(TokenType.Filter)
784
+ this.expect(TokenType.LParen)
785
+ const expression = this.parseExpression()
786
+ this.expect(TokenType.RParen)
787
+ return {
788
+ kind: 'FilterClause',
789
+ expression,
790
+ range: { start, end: this.endPos() }
791
+ }
792
+ }
793
+
794
+ private parseNotClause(): NotClause {
795
+ const start = this.currentPos()
796
+ this.expect(TokenType.Not)
797
+ this.expect(TokenType.LBrace)
798
+ const patterns = this.parseWherePatterns()
799
+ this.expect(TokenType.RBrace)
800
+ return {
801
+ kind: 'NotClause',
802
+ patterns,
803
+ range: { start, end: this.endPos() }
804
+ }
805
+ }
806
+
807
+ private parseOptionalClause(): OptionalClause {
808
+ const start = this.currentPos()
809
+ this.expect(TokenType.Optional)
810
+ this.expect(TokenType.LBrace)
811
+ const patterns = this.parseWherePatterns()
812
+ this.expect(TokenType.RBrace)
813
+ return {
814
+ kind: 'OptionalClause',
815
+ patterns,
816
+ range: { start, end: this.endPos() }
817
+ }
818
+ }
819
+
820
+ private parseUnionClause(): UnionClause {
821
+ const start = this.currentPos()
822
+ this.expect(TokenType.Union)
823
+ this.expect(TokenType.LBrace)
824
+ const patterns = this.parseWherePatterns()
825
+ this.expect(TokenType.RBrace)
826
+ return {
827
+ kind: 'UnionClause',
828
+ patterns,
829
+ range: { start, end: this.endPos() }
830
+ }
831
+ }
832
+
833
+ // ────────────────────────────────────────────────────────────────────
834
+ // Raw semantic tuples
835
+ // ────────────────────────────────────────────────────────────────────
836
+
837
+ private parsePropositionTuple(): PropositionTuple {
838
+ const start = this.currentPos()
839
+ this.expect(TokenType.LParen)
840
+
841
+ // `(id: ...)` addresses the same slot by record identity. `id` is a field
842
+ // name, not a keyword, so it is matched on its exact lowercase text and
843
+ // only when a `:` follows — `(id, "p", ?o)` is still a triple whose
844
+ // subject happens to be a variable-free term.
845
+ if (this.isPropositionIdStart()) {
846
+ this.advance() // id
847
+ this.advance() // :
848
+ const id = this.parseScalarValue()
849
+ this.expect(TokenType.RParen)
850
+ return {
851
+ kind: 'PropositionTuple',
852
+ id,
853
+ range: { start, end: this.endPos() }
854
+ }
855
+ }
856
+
857
+ const subject = this.parseTerm()
858
+ this.expect(TokenType.Comma)
859
+ const predicate = this.parseRawPredicateExpression()
860
+ this.expect(TokenType.Comma)
861
+ const object = this.parseTerm()
862
+ this.expect(TokenType.RParen)
863
+ return {
864
+ kind: 'PropositionTuple',
865
+ subject,
866
+ predicate,
867
+ object,
868
+ range: { start, end: this.endPos() }
869
+ }
870
+ }
871
+
872
+ /**
873
+ * True when the cursor sits on the `id :` of a `(id: ...)` reference.
874
+ *
875
+ * Field names are case-sensitive, so only the exact spelling `id` counts;
876
+ * `ID` is an ordinary identifier and would not parse as a term anyway.
877
+ */
878
+ private isPropositionIdStart(): boolean {
879
+ const tok = this.current()
880
+ if (tok.type !== TokenType.Identifier || tok.value !== 'id') return false
881
+ const next = this.peekPast(1)
882
+ // `(id: :p)` lexes the separator as its own colon; `(id:"P")` likewise,
883
+ // because a parameter needs an identifier start after the colon.
884
+ return next?.type === TokenType.Colon
885
+ }
886
+
887
+ private parseTerm(): Term {
888
+ const tok = this.current()
889
+ switch (tok.type) {
890
+ case TokenType.Variable:
891
+ return this.parseVariableRef()
892
+ case TokenType.Parameter:
893
+ return this.parseParameterRef()
894
+ case TokenType.LBrace:
895
+ return this.parseObjectPattern()
896
+ case TokenType.LParen:
897
+ return this.parsePropositionTuple()
898
+ case TokenType.String:
899
+ case TokenType.Number:
900
+ case TokenType.Boolean:
901
+ case TokenType.Null:
902
+ return this.parseLiteral()
903
+ default:
904
+ this.error(
905
+ `Expected a term (variable, parameter, literal, {...} or a tuple) but got '${tok.value}'`,
906
+ tok
907
+ )
908
+ throw new ParseAbort()
909
+ }
910
+ }
911
+
912
+ /** `predicate_atom = string_literal | parameter | variable` */
913
+ private parsePredicateAtom(): PredicateAtom {
914
+ const tok = this.current()
915
+ if (tok.type === TokenType.String) {
916
+ return this.parseStringLiteral()
917
+ }
918
+ if (tok.type === TokenType.Parameter) {
919
+ return this.parseParameterRef()
920
+ }
921
+ if (tok.type === TokenType.Variable) {
922
+ return this.parseVariableRef()
923
+ }
924
+ this.error(
925
+ `Expected a predicate (quoted symbol, :parameter or ?variable) but got '${tok.value}'`,
926
+ tok
927
+ )
928
+ throw new ParseAbort()
929
+ }
930
+
931
+ /**
932
+ * `raw_predicate_expression` — path atoms joined by `|`.
933
+ *
934
+ * Alternation and hop quantifiers are traversal syntax owned by KQL. KML
935
+ * and META spell the same slot as a bare `predicate_atom`, so in the raw
936
+ * dialect anything beyond one plain atom is reported here.
937
+ */
938
+ private parseRawPredicateExpression(): RawPredicateExpression {
939
+ const start = this.currentPos()
940
+ const atoms: PredicatePathAtom[] = [this.parsePredicatePathAtom()]
941
+ while (this.check(TokenType.Pipe)) {
942
+ const pipe = this.current()
943
+ if (this.dialect === 'raw') {
944
+ this.error(
945
+ 'Predicate alternation is a KQL traversal form and is not allowed here',
946
+ pipe
947
+ )
948
+ }
949
+ this.advance()
950
+ atoms.push(this.parsePredicatePathAtom())
951
+ }
952
+ return {
953
+ kind: 'RawPredicateExpression',
954
+ atoms,
955
+ range: { start, end: this.endPos() }
956
+ }
957
+ }
958
+
959
+ private parsePredicatePathAtom(): PredicatePathAtom {
960
+ const start = this.currentPos()
961
+ const atom = this.parsePredicateAtom()
962
+ let quantifier: PathQuantifier | undefined
963
+ if (this.check(TokenType.LBrace)) {
964
+ const brace = this.current()
965
+ if (this.dialect === 'raw') {
966
+ this.error(
967
+ 'Path quantifiers are a KQL traversal form and are not allowed here',
968
+ brace
969
+ )
970
+ }
971
+ quantifier = this.parsePathQuantifier()
972
+ }
973
+ return {
974
+ kind: 'PredicatePathAtom',
975
+ atom,
976
+ quantifier,
977
+ range: { start, end: this.endPos() }
978
+ }
979
+ }
980
+
981
+ private parsePathQuantifier(): PathQuantifier {
982
+ const start = this.currentPos()
983
+ this.expect(TokenType.LBrace)
984
+ const min = this.expectHopCount()
985
+ let max: number | undefined
986
+ let hasComma = false
987
+ if (this.match(TokenType.Comma)) {
988
+ hasComma = true
989
+ if (!this.check(TokenType.RBrace)) {
990
+ max = this.expectHopCount()
991
+ }
992
+ } else {
993
+ max = min
994
+ }
995
+ this.expect(TokenType.RBrace)
996
+ if (max !== undefined && max < min) {
997
+ this.error(
998
+ `Hop range {${min},${max}} is empty: the maximum is below the minimum`,
999
+ this.current()
1000
+ )
1001
+ }
1002
+ return {
1003
+ kind: 'PathQuantifier',
1004
+ min,
1005
+ max,
1006
+ hasComma,
1007
+ range: { start, end: this.endPos() }
1008
+ }
1009
+ }
1010
+
1011
+ /**
1012
+ * A hop count is a plain unsigned integer.
1013
+ *
1014
+ * `{1.5}`, `{-1}` and `{1e3}` all lex as one number token, so the check is
1015
+ * on the token text, not on the parsed value.
1016
+ */
1017
+ private expectHopCount(): number {
1018
+ const tok = this.current()
1019
+ if (tok.type !== TokenType.Number || !/^\d+$/.test(tok.value)) {
1020
+ this.error(
1021
+ `Expected an unsigned integer hop count but got '${tok.value}'`,
1022
+ tok
1023
+ )
1024
+ this.advance()
1025
+ return 0
1026
+ }
1027
+ const value = Number(tok.value)
1028
+ if (value > 65535) {
1029
+ this.error(`Hop count ${value} exceeds the 16-bit maximum 65535`, tok)
1030
+ }
1031
+ this.advance()
1032
+ return value
1033
+ }
1034
+
1035
+ // ────────────────────────────────────────────────────────────────────
1036
+ // KML — MUTATE
1037
+ // ────────────────────────────────────────────────────────────────────
1038
+
1039
+ private parseMutateStatement(): MutateStatement {
1040
+ const leadingComments = this.collectLeadingComments()
1041
+ const start = this.currentPos()
1042
+ this.expectKeywordWithSpace(TokenType.Mutate)
1043
+ this.expect(TokenType.LBrace)
1044
+
1045
+ const clauses: MutationClause[] = []
1046
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1047
+ const before = this.pos
1048
+ this.skipComments()
1049
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1050
+ // A nested MUTATE is not a smaller transaction, it is a different
1051
+ // statement; the grammar forbids it outright.
1052
+ if (this.check(TokenType.Mutate)) {
1053
+ this.error('MUTATE cannot contain another MUTATE', this.current())
1054
+ this.advance()
1055
+ continue
1056
+ }
1057
+ try {
1058
+ clauses.push(this.parseMutationClause())
1059
+ } catch {
1060
+ this.recoverToMutationBoundary()
1061
+ }
1062
+ if (this.pos === before) break
1063
+ }
1064
+ this.expect(TokenType.RBrace)
1065
+
1066
+ return {
1067
+ kind: 'MutateStatement',
1068
+ clauses,
1069
+ range: { start, end: this.endPos() },
1070
+ leadingComments: leadingComments.length ? leadingComments : undefined
1071
+ }
1072
+ }
1073
+
1074
+ // ────────────────────────────────────────────────────────────────────
1075
+ // KML — CREATE / UPSERT / ENSURE / ASSERT
1076
+ // ────────────────────────────────────────────────────────────────────
1077
+
1078
+ private parseCreateStatement():
1079
+ | CreateConceptStatement
1080
+ | CreateEvidenceStatement
1081
+ | CreateAssertionStatement
1082
+ | CreateActivityStatement {
1083
+ const leadingComments = this.collectLeadingComments()
1084
+ const start = this.currentPos()
1085
+ const create = this.expectKeywordWithSpace(TokenType.Create)
1086
+
1087
+ const tok = this.current()
1088
+ switch (tok.type) {
1089
+ case TokenType.Concept:
1090
+ this.expectSecondWord(TokenType.Concept, create)
1091
+ return this.parseCreateConceptBody(start, leadingComments)
1092
+ case TokenType.Evidence:
1093
+ this.expectSecondWord(TokenType.Evidence, create)
1094
+ return this.parseRecordCreateBody(
1095
+ 'CreateEvidenceStatement',
1096
+ start,
1097
+ leadingComments
1098
+ ) as CreateEvidenceStatement
1099
+ case TokenType.Assertion:
1100
+ this.expectSecondWord(TokenType.Assertion, create)
1101
+ return this.parseRecordCreateBody(
1102
+ 'CreateAssertionStatement',
1103
+ start,
1104
+ leadingComments
1105
+ ) as CreateAssertionStatement
1106
+ case TokenType.Activity:
1107
+ this.expectSecondWord(TokenType.Activity, create)
1108
+ return this.parseRecordCreateBody(
1109
+ 'CreateActivityStatement',
1110
+ start,
1111
+ leadingComments
1112
+ ) as CreateActivityStatement
1113
+ default:
1114
+ this.error(
1115
+ `Expected CONCEPT, EVIDENCE, ASSERTION or ACTIVITY after CREATE but got '${tok.value}'`,
1116
+ tok
1117
+ )
1118
+ throw new ParseAbort()
1119
+ }
1120
+ }
1121
+
1122
+ private parseCreateConceptBody(
1123
+ start: Position,
1124
+ leadingComments: string[]
1125
+ ): CreateConceptStatement {
1126
+ const handle = this.expectHandle()
1127
+ this.expect(TokenType.LBrace)
1128
+
1129
+ const stmt: CreateConceptStatement = {
1130
+ kind: 'CreateConceptStatement',
1131
+ handle,
1132
+ setFacets: [],
1133
+ range: { start, end: start },
1134
+ leadingComments: leadingComments.length ? leadingComments : undefined
1135
+ }
1136
+
1137
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1138
+ const before = this.pos
1139
+ this.skipComments()
1140
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1141
+ const tok = this.current()
1142
+ switch (tok.type) {
1143
+ case TokenType.Type:
1144
+ this.rejectRepeat(stmt.type, 'TYPE', tok)
1145
+ stmt.type = this.parseTypeClause()
1146
+ break
1147
+ case TokenType.Client:
1148
+ this.rejectRepeat(stmt.clientKey, 'CLIENT KEY', tok)
1149
+ stmt.clientKey = this.parseClientKeyClause()
1150
+ break
1151
+ case TokenType.Name:
1152
+ this.rejectRepeat(stmt.name, 'NAME', tok)
1153
+ stmt.name = this.parseNameClause()
1154
+ break
1155
+ case TokenType.Set:
1156
+ this.applyCreateSetClause(stmt, tok)
1157
+ break
1158
+ default:
1159
+ this.error(`Unexpected token '${tok.value}' in CREATE CONCEPT`, tok)
1160
+ this.advance()
1161
+ }
1162
+ if (this.pos === before) break
1163
+ }
1164
+ this.expect(TokenType.RBrace)
1165
+ stmt.range = { start, end: this.endPos() }
1166
+ return stmt
1167
+ }
1168
+
1169
+ private applyCreateSetClause(stmt: CreateConceptStatement, tok: Token): void {
1170
+ const clause = this.parseSetClause()
1171
+ switch (clause.kind) {
1172
+ case 'SetFieldsClause':
1173
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok)
1174
+ stmt.setFields = clause
1175
+ break
1176
+ case 'SetAttributesClause':
1177
+ this.rejectRepeat(stmt.setAttributes, 'SET ATTRIBUTES', tok)
1178
+ stmt.setAttributes = clause
1179
+ break
1180
+ case 'SetFacetClause':
1181
+ stmt.setFacets.push(clause)
1182
+ break
1183
+ case 'SetStructuralClause':
1184
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok)
1185
+ stmt.setStructural = clause
1186
+ break
1187
+ default:
1188
+ this.error(`'SET ${clause.kind}' is not allowed in CREATE CONCEPT`, tok)
1189
+ }
1190
+ }
1191
+
1192
+ /** CREATE EVIDENCE / ASSERTION / ACTIVITY share one clause vocabulary. */
1193
+ private parseRecordCreateBody(
1194
+ kind:
1195
+ | 'CreateEvidenceStatement'
1196
+ | 'CreateAssertionStatement'
1197
+ | 'CreateActivityStatement',
1198
+ start: Position,
1199
+ leadingComments: string[]
1200
+ ): CreateEvidenceStatement | CreateAssertionStatement | CreateActivityStatement {
1201
+ const handle = this.expectHandle()
1202
+ this.expect(TokenType.LBrace)
1203
+
1204
+ const stmt = {
1205
+ kind,
1206
+ handle,
1207
+ setFacets: [] as SetFacetClause[],
1208
+ clientKey: undefined as ClientKeyClause | undefined,
1209
+ setFields: undefined as SetFieldsClause | undefined,
1210
+ setStructural: undefined as SetStructuralClause | undefined,
1211
+ range: { start, end: start },
1212
+ leadingComments: leadingComments.length ? leadingComments : undefined
1213
+ }
1214
+
1215
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1216
+ const before = this.pos
1217
+ this.skipComments()
1218
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1219
+ const tok = this.current()
1220
+ if (tok.type === TokenType.Client) {
1221
+ this.rejectRepeat(stmt.clientKey, 'CLIENT KEY', tok)
1222
+ stmt.clientKey = this.parseClientKeyClause()
1223
+ } else if (tok.type === TokenType.Set) {
1224
+ const clause = this.parseSetClause()
1225
+ if (clause.kind === 'SetFieldsClause') {
1226
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok)
1227
+ stmt.setFields = clause
1228
+ } else if (clause.kind === 'SetFacetClause') {
1229
+ stmt.setFacets.push(clause)
1230
+ } else if (clause.kind === 'SetStructuralClause') {
1231
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok)
1232
+ stmt.setStructural = clause
1233
+ } else {
1234
+ this.error(
1235
+ `'SET ${clause.kind}' is not allowed in ${kind.replace('Statement', '')}`,
1236
+ tok
1237
+ )
1238
+ }
1239
+ } else {
1240
+ this.error(`Unexpected token '${tok.value}' in ${kind}`, tok)
1241
+ this.advance()
1242
+ }
1243
+ if (this.pos === before) break
1244
+ }
1245
+ this.expect(TokenType.RBrace)
1246
+ stmt.range = { start, end: this.endPos() }
1247
+ return stmt as
1248
+ | CreateEvidenceStatement
1249
+ | CreateAssertionStatement
1250
+ | CreateActivityStatement
1251
+ }
1252
+
1253
+ private parseUpsertConcept(): UpsertConceptStatement {
1254
+ const leadingComments = this.collectLeadingComments()
1255
+ const start = this.currentPos()
1256
+ const upsert = this.expectKeywordWithSpace(TokenType.Upsert)
1257
+ this.expectSecondWord(TokenType.Concept, upsert)
1258
+ const handle = this.expectHandle()
1259
+ this.expect(TokenType.LBrace)
1260
+
1261
+ const stmt: UpsertConceptStatement = {
1262
+ kind: 'UpsertConceptStatement',
1263
+ handle,
1264
+ setFacets: [],
1265
+ unsetFacets: [],
1266
+ range: { start, end: start },
1267
+ leadingComments: leadingComments.length ? leadingComments : undefined
1268
+ }
1269
+
1270
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1271
+ const before = this.pos
1272
+ this.skipComments()
1273
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1274
+ const tok = this.current()
1275
+ switch (tok.type) {
1276
+ case TokenType.Match:
1277
+ this.rejectRepeat(stmt.match, 'MATCH', tok)
1278
+ stmt.match = this.parseMatchClause()
1279
+ break
1280
+ case TokenType.Expect:
1281
+ this.rejectRepeat(stmt.expectVersion, 'EXPECT VERSION', tok)
1282
+ stmt.expectVersion = this.parseExpectVersionClause()
1283
+ break
1284
+ case TokenType.Set: {
1285
+ const clause = this.parseSetClause()
1286
+ if (clause.kind === 'SetFieldsClause') {
1287
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok)
1288
+ stmt.setFields = clause
1289
+ } else if (clause.kind === 'SetAttributesClause') {
1290
+ this.rejectRepeat(stmt.setAttributes, 'SET ATTRIBUTES', tok)
1291
+ stmt.setAttributes = clause
1292
+ } else if (clause.kind === 'SetFacetClause') {
1293
+ stmt.setFacets.push(clause)
1294
+ } else if (clause.kind === 'SetStructuralClause') {
1295
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok)
1296
+ stmt.setStructural = clause
1297
+ } else {
1298
+ this.error(`'SET RETENTION' is not a clause of UPSERT CONCEPT`, tok)
1299
+ }
1300
+ break
1301
+ }
1302
+ case TokenType.Unset: {
1303
+ const clause = this.parseUnsetClause()
1304
+ if (clause.kind === 'UnsetAttributesClause') {
1305
+ this.rejectRepeat(stmt.unsetAttributes, 'UNSET ATTRIBUTES', tok)
1306
+ stmt.unsetAttributes = clause
1307
+ } else if (clause.kind === 'UnsetStructuralClause') {
1308
+ this.rejectRepeat(stmt.unsetStructural, 'UNSET STRUCTURAL', tok)
1309
+ stmt.unsetStructural = clause
1310
+ } else {
1311
+ stmt.unsetFacets.push(clause)
1312
+ }
1313
+ break
1314
+ }
1315
+ default:
1316
+ this.error(`Unexpected token '${tok.value}' in UPSERT CONCEPT`, tok)
1317
+ this.advance()
1318
+ }
1319
+ if (this.pos === before) break
1320
+ }
1321
+ this.expect(TokenType.RBrace)
1322
+ stmt.range = { start, end: this.endPos() }
1323
+ return stmt
1324
+ }
1325
+
1326
+ private parseEnsureProposition(): EnsurePropositionStatement {
1327
+ const leadingComments = this.collectLeadingComments()
1328
+ const start = this.currentPos()
1329
+ const ensure = this.expectKeywordWithSpace(TokenType.Ensure)
1330
+ this.expectSecondWord(TokenType.Proposition, ensure)
1331
+
1332
+ const handle = this.check(TokenType.Variable)
1333
+ ? this.parseVariableRef()
1334
+ : undefined
1335
+ this.dialect = 'raw'
1336
+ const tuple = this.parsePropositionTuple()
1337
+ const expectVersion = this.check(TokenType.Expect)
1338
+ ? this.parseExpectVersionClause()
1339
+ : undefined
1340
+
1341
+ return {
1342
+ kind: 'EnsurePropositionStatement',
1343
+ handle,
1344
+ tuple,
1345
+ expectVersion,
1346
+ range: { start, end: this.endPos() },
1347
+ leadingComments: leadingComments.length ? leadingComments : undefined
1348
+ }
1349
+ }
1350
+
1351
+ private parseAssertStatement(): AssertStatement {
1352
+ const leadingComments = this.collectLeadingComments()
1353
+ const start = this.currentPos()
1354
+ this.expectKeywordWithSpace(TokenType.Assert)
1355
+
1356
+ const handle = this.check(TokenType.Variable)
1357
+ ? this.parseVariableRef()
1358
+ : undefined
1359
+ this.dialect = 'raw'
1360
+ const tuple = this.parsePropositionTuple()
1361
+ const assignments = this.parseAssignmentObject()
1362
+
1363
+ let superseding: TargetRef | undefined
1364
+ if (this.match(TokenType.Superseding)) {
1365
+ superseding = this.parseTargetRef()
1366
+ }
1367
+
1368
+ return {
1369
+ kind: 'AssertStatement',
1370
+ handle,
1371
+ tuple,
1372
+ assignments,
1373
+ superseding,
1374
+ range: { start, end: this.endPos() },
1375
+ leadingComments: leadingComments.length ? leadingComments : undefined
1376
+ }
1377
+ }
1378
+
1379
+ // ────────────────────────────────────────────────────────────────────
1380
+ // KML — clause vocabulary
1381
+ // ────────────────────────────────────────────────────────────────────
1382
+
1383
+ private parseTypeClause(): TypeClause {
1384
+ const start = this.currentPos()
1385
+ this.expect(TokenType.Type)
1386
+ const value = this.parseSchemaSymbol()
1387
+ return { kind: 'TypeClause', value, range: { start, end: this.endPos() } }
1388
+ }
1389
+
1390
+ private parseClientKeyClause(): ClientKeyClause {
1391
+ const start = this.currentPos()
1392
+ const client = this.expect(TokenType.Client)
1393
+ this.expectSecondWord(TokenType.Key, client)
1394
+ const value = this.parseScalarValue()
1395
+ return {
1396
+ kind: 'ClientKeyClause',
1397
+ value,
1398
+ range: { start, end: this.endPos() }
1399
+ }
1400
+ }
1401
+
1402
+ private parseNameClause(): NameClause {
1403
+ const start = this.currentPos()
1404
+ this.expect(TokenType.Name)
1405
+ const value = this.parseScalarValue()
1406
+ return { kind: 'NameClause', value, range: { start, end: this.endPos() } }
1407
+ }
1408
+
1409
+ private parseMatchClause(): MatchClause {
1410
+ const start = this.currentPos()
1411
+ this.expect(TokenType.Match)
1412
+ const pattern = this.parseObjectPattern()
1413
+ return {
1414
+ kind: 'MatchClause',
1415
+ pattern,
1416
+ range: { start, end: this.endPos() }
1417
+ }
1418
+ }
1419
+
1420
+ /** Dispatches every `SET ...` form; callers reject the ones they disallow. */
1421
+ private parseSetClause():
1422
+ | SetFieldsClause
1423
+ | SetAttributesClause
1424
+ | SetFacetClause
1425
+ | SetStructuralClause
1426
+ | { kind: 'SetRetentionMarker' } {
1427
+ const start = this.currentPos()
1428
+ const set = this.expect(TokenType.Set)
1429
+ const tok = this.current()
1430
+
1431
+ switch (tok.type) {
1432
+ case TokenType.Fields: {
1433
+ this.expectSecondWord(TokenType.Fields, set)
1434
+ const assignments = this.parseAssignmentObject()
1435
+ return {
1436
+ kind: 'SetFieldsClause',
1437
+ assignments,
1438
+ range: { start, end: this.endPos() }
1439
+ }
1440
+ }
1441
+ case TokenType.Attributes: {
1442
+ this.expectSecondWord(TokenType.Attributes, set)
1443
+ const assignments = this.parseAssignmentObject()
1444
+ return {
1445
+ kind: 'SetAttributesClause',
1446
+ assignments,
1447
+ range: { start, end: this.endPos() }
1448
+ }
1449
+ }
1450
+ case TokenType.Facet: {
1451
+ this.expectSecondWord(TokenType.Facet, set)
1452
+ const facet = this.parseSchemaSymbol()
1453
+ const assignments = this.parseAssignmentObject()
1454
+ return {
1455
+ kind: 'SetFacetClause',
1456
+ facet,
1457
+ assignments,
1458
+ range: { start, end: this.endPos() }
1459
+ }
1460
+ }
1461
+ case TokenType.Structural: {
1462
+ this.expectSecondWord(TokenType.Structural, set)
1463
+ return this.parseSetStructuralBody(start)
1464
+ }
1465
+ case TokenType.Retention:
1466
+ return { kind: 'SetRetentionMarker' }
1467
+ default:
1468
+ this.error(
1469
+ `Expected FIELDS, ATTRIBUTES, FACET, STRUCTURAL or RETENTION after SET but got '${tok.value}'`,
1470
+ tok
1471
+ )
1472
+ throw new ParseAbort()
1473
+ }
1474
+ }
1475
+
1476
+ private parseSetStructuralBody(start: Position): SetStructuralClause {
1477
+ this.expect(TokenType.LBrace)
1478
+ const assignments: StructuralAssignment[] = []
1479
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1480
+ const before = this.pos
1481
+ this.skipComments()
1482
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1483
+ assignments.push(this.parseStructuralAssignment())
1484
+ if (this.pos === before) break
1485
+ }
1486
+ this.expect(TokenType.RBrace)
1487
+ return {
1488
+ kind: 'SetStructuralClause',
1489
+ assignments,
1490
+ range: { start, end: this.endPos() }
1491
+ }
1492
+ }
1493
+
1494
+ private parseStructuralAssignment(): StructuralAssignment {
1495
+ const start = this.currentPos()
1496
+ this.expect(TokenType.LParen)
1497
+ const field = this.parseSchemaSymbol()
1498
+ this.expect(TokenType.Comma)
1499
+ const value = this.parseMutationValue()
1500
+ this.expect(TokenType.RParen)
1501
+
1502
+ const options = this.check(TokenType.LBrace)
1503
+ ? this.parseObjectLiteral()
1504
+ : undefined
1505
+
1506
+ return {
1507
+ kind: 'StructuralAssignment',
1508
+ field,
1509
+ value,
1510
+ options,
1511
+ range: { start, end: this.endPos() }
1512
+ }
1513
+ }
1514
+
1515
+ private parseUnsetClause():
1516
+ | UnsetAttributesClause
1517
+ | UnsetFacetClause
1518
+ | UnsetStructuralClause {
1519
+ const start = this.currentPos()
1520
+ const unset = this.expect(TokenType.Unset)
1521
+ const tok = this.current()
1522
+
1523
+ if (tok.type === TokenType.Attributes) {
1524
+ this.expectSecondWord(TokenType.Attributes, unset)
1525
+ const fields = this.parseUnsetFieldSet()
1526
+ return {
1527
+ kind: 'UnsetAttributesClause',
1528
+ fields,
1529
+ range: { start, end: this.endPos() }
1530
+ }
1531
+ }
1532
+ if (tok.type === TokenType.Facet) {
1533
+ this.expectSecondWord(TokenType.Facet, unset)
1534
+ const facet = this.parseSchemaSymbol()
1535
+ const fields = this.parseUnsetFieldSet()
1536
+ return {
1537
+ kind: 'UnsetFacetClause',
1538
+ facet,
1539
+ fields,
1540
+ range: { start, end: this.endPos() }
1541
+ }
1542
+ }
1543
+ if (tok.type === TokenType.Structural) {
1544
+ // Every SET has an UNSET: an entry is the SET STRUCTURAL entry without
1545
+ // its options object (Spec §17.5).
1546
+ this.expectSecondWord(TokenType.Structural, unset)
1547
+ this.expect(TokenType.LBrace)
1548
+ const removals: StructuralRemoval[] = []
1549
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1550
+ const before = this.pos
1551
+ this.skipComments()
1552
+ if (this.check(TokenType.RBrace) || this.isAtEnd()) break
1553
+ removals.push(this.parseStructuralRemoval())
1554
+ if (this.pos === before) break
1555
+ }
1556
+ this.expect(TokenType.RBrace)
1557
+ return {
1558
+ kind: 'UnsetStructuralClause',
1559
+ removals,
1560
+ range: { start, end: this.endPos() }
1561
+ }
1562
+ }
1563
+ this.error(
1564
+ `Expected ATTRIBUTES, FACET or STRUCTURAL after UNSET but got '${tok.value}'`,
1565
+ tok
1566
+ )
1567
+ throw new ParseAbort()
1568
+ }
1569
+
1570
+ private parseStructuralRemoval(): StructuralRemoval {
1571
+ const start = this.currentPos()
1572
+ this.expect(TokenType.LParen)
1573
+ const field = this.parseSchemaSymbol()
1574
+ this.expect(TokenType.Comma)
1575
+ const value = this.parseMutationValue()
1576
+ this.expect(TokenType.RParen)
1577
+ if (this.check(TokenType.LBrace)) {
1578
+ this.error(
1579
+ 'UNSET STRUCTURAL removes a reference by (field, target); it takes no options object',
1580
+ this.current()
1581
+ )
1582
+ }
1583
+ return {
1584
+ kind: 'StructuralRemoval',
1585
+ field,
1586
+ value,
1587
+ range: { start, end: this.endPos() }
1588
+ }
1589
+ }
1590
+
1591
+ private parseUnsetFieldSet(): UnsetField[] {
1592
+ this.expect(TokenType.LBrace)
1593
+ const fields: UnsetField[] = []
1594
+ if (!this.check(TokenType.RBrace)) {
1595
+ do {
1596
+ if (this.check(TokenType.RBrace)) break
1597
+ const start = this.currentPos()
1598
+ const { key, isQuoted } = this.expectKeyWithQuoting()
1599
+ fields.push({
1600
+ kind: 'UnsetField',
1601
+ name: key,
1602
+ isQuoted,
1603
+ range: { start, end: this.endPos() }
1604
+ })
1605
+ } while (this.match(TokenType.Comma))
1606
+ }
1607
+ this.expect(TokenType.RBrace)
1608
+ return fields
1609
+ }
1610
+
1611
+ private parseExpectVersionClause(): ExpectVersionClause {
1612
+ const start = this.currentPos()
1613
+ const expect = this.expect(TokenType.Expect)
1614
+ this.expectSecondWord(TokenType.Version, expect)
1615
+ const value = this.parseScalarValue()
1616
+ return {
1617
+ kind: 'ExpectVersionClause',
1618
+ value,
1619
+ range: { start, end: this.endPos() }
1620
+ }
1621
+ }
1622
+
1623
+ private parseExpectStateClause(): ExpectStateClause {
1624
+ const start = this.currentPos()
1625
+ const expect = this.expect(TokenType.Expect)
1626
+ this.expectSecondWord(TokenType.State, expect)
1627
+ const value = this.parseScalarValue()
1628
+ return {
1629
+ kind: 'ExpectStateClause',
1630
+ value,
1631
+ range: { start, end: this.endPos() }
1632
+ }
1633
+ }
1634
+
1635
+ // ────────────────────────────────────────────────────────────────────
1636
+ // KML — UPDATE
1637
+ // ────────────────────────────────────────────────────────────────────
1638
+
1639
+ private parseUpdateStatement(): UpdateStatement {
1640
+ const leadingComments = this.collectLeadingComments()
1641
+ const start = this.currentPos()
1642
+ this.expectKeywordWithSpace(TokenType.Update)
1643
+ const target = this.parseTargetRef()
1644
+
1645
+ const expectVersion = this.check(TokenType.Expect)
1646
+ ? this.parseExpectVersionClause()
1647
+ : undefined
1648
+
1649
+ const actions: UpdateAction[] = []
1650
+ for (;;) {
1651
+ if (this.check(TokenType.Set)) {
1652
+ const clause = this.parseSetClause()
1653
+ if (clause.kind === 'SetRetentionMarker') {
1654
+ this.error(
1655
+ 'SET RETENTION is its own statement, not an UPDATE action',
1656
+ this.current()
1657
+ )
1658
+ break
1659
+ }
1660
+ actions.push(clause)
1661
+ } else if (this.check(TokenType.Unset)) {
1662
+ actions.push(this.parseUnsetClause())
1663
+ } else {
1664
+ break
1665
+ }
1666
+ }
1667
+ if (actions.length === 0) {
1668
+ this.error('UPDATE requires at least one SET or UNSET action', this.current())
1669
+ }
1670
+
1671
+ // WHERE binds a ?variable target; a direct :id / "id" target already names
1672
+ // the element and may omit it, exactly as ARCHIVE / TOMBSTONE / PURGE /
1673
+ // SET RETENTION / RETRACT ASSERTION do (Spec §58). Whether a bare
1674
+ // ?variable is bound is semantic — inside MUTATE it may be a local handle.
1675
+ let where: WhereClause | undefined
1676
+ if (this.check(TokenType.Where)) {
1677
+ this.expectKeywordWithSpace(TokenType.Where)
1678
+ this.dialect = 'raw'
1679
+ where = this.parseWhereClause()
1680
+ }
1681
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1682
+
1683
+ return {
1684
+ kind: 'UpdateStatement',
1685
+ target,
1686
+ expectVersion,
1687
+ actions,
1688
+ where,
1689
+ limit,
1690
+ range: { start, end: this.endPos() },
1691
+ leadingComments: leadingComments.length ? leadingComments : undefined
1692
+ }
1693
+ }
1694
+
1695
+ // ────────────────────────────────────────────────────────────────────
1696
+ // KML — lifecycle and correction
1697
+ // ────────────────────────────────────────────────────────────────────
1698
+
1699
+ private parseRetractAssertion(): RetractAssertionStatement {
1700
+ const leadingComments = this.collectLeadingComments()
1701
+ const start = this.currentPos()
1702
+ const retract = this.expectKeywordWithSpace(TokenType.Retract)
1703
+ this.expectSecondWord(TokenType.Assertion, retract)
1704
+ const target = this.parseTargetRef()
1705
+
1706
+ let where: WhereClause | undefined
1707
+ if (this.check(TokenType.Where)) {
1708
+ this.expectKeywordWithSpace(TokenType.Where)
1709
+ this.dialect = 'raw'
1710
+ where = this.parseWhereClause()
1711
+ }
1712
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1713
+ const expectState = this.check(TokenType.Expect)
1714
+ ? this.parseExpectStateClause()
1715
+ : undefined
1716
+
1717
+ return {
1718
+ kind: 'RetractAssertionStatement',
1719
+ target,
1720
+ where,
1721
+ limit,
1722
+ expectState,
1723
+ range: { start, end: this.endPos() },
1724
+ leadingComments: leadingComments.length ? leadingComments : undefined
1725
+ }
1726
+ }
1727
+
1728
+ private parseSupersedeAssertion(): SupersedeAssertionStatement {
1729
+ const leadingComments = this.collectLeadingComments()
1730
+ const start = this.currentPos()
1731
+ const supersede = this.expectKeywordWithSpace(TokenType.Supersede)
1732
+ this.expectSecondWord(TokenType.Assertion, supersede)
1733
+ const target = this.parseTargetRef()
1734
+ this.expect(TokenType.By)
1735
+ const by = this.parseTargetRef()
1736
+ const expectState = this.check(TokenType.Expect)
1737
+ ? this.parseExpectStateClause()
1738
+ : undefined
1739
+
1740
+ return {
1741
+ kind: 'SupersedeAssertionStatement',
1742
+ target,
1743
+ by,
1744
+ expectState,
1745
+ range: { start, end: this.endPos() },
1746
+ leadingComments: leadingComments.length ? leadingComments : undefined
1747
+ }
1748
+ }
1749
+
1750
+ private parseCorrectEvidence(): CorrectEvidenceStatement {
1751
+ const leadingComments = this.collectLeadingComments()
1752
+ const start = this.currentPos()
1753
+ const correct = this.expectKeywordWithSpace(TokenType.Correct)
1754
+ this.expectSecondWord(TokenType.Evidence, correct)
1755
+ const target = this.parseTargetRef()
1756
+ this.expect(TokenType.By)
1757
+ const by = this.parseTargetRef()
1758
+ const expectState = this.check(TokenType.Expect)
1759
+ ? this.parseExpectStateClause()
1760
+ : undefined
1761
+
1762
+ return {
1763
+ kind: 'CorrectEvidenceStatement',
1764
+ target,
1765
+ by,
1766
+ expectState,
1767
+ range: { start, end: this.endPos() },
1768
+ leadingComments: leadingComments.length ? leadingComments : undefined
1769
+ }
1770
+ }
1771
+
1772
+ private parseTransitionActivity(): TransitionActivityStatement {
1773
+ const leadingComments = this.collectLeadingComments()
1774
+ const start = this.currentPos()
1775
+ const transition = this.expectKeywordWithSpace(TokenType.Transition)
1776
+ this.expectSecondWord(TokenType.Activity, transition)
1777
+ const target = this.parseTargetRef()
1778
+ this.expect(TokenType.To)
1779
+ const to = this.parseScalarValue()
1780
+
1781
+ // Terminal outputs and ended_at may be finalized in the same statement
1782
+ // that moves the Activity to its terminal state.
1783
+ const finalize: (SetFieldsClause | SetStructuralClause)[] = []
1784
+ while (this.check(TokenType.Set)) {
1785
+ const clause = this.parseSetClause()
1786
+ if (clause.kind === 'SetFieldsClause' || clause.kind === 'SetStructuralClause') {
1787
+ finalize.push(clause)
1788
+ } else {
1789
+ this.error(
1790
+ 'TRANSITION ACTIVITY accepts only SET FIELDS and SET STRUCTURAL',
1791
+ this.current()
1792
+ )
1793
+ break
1794
+ }
1795
+ }
1796
+
1797
+ const expectState = this.check(TokenType.Expect)
1798
+ ? this.parseExpectStateClause()
1799
+ : undefined
1800
+
1801
+ return {
1802
+ kind: 'TransitionActivityStatement',
1803
+ target,
1804
+ to,
1805
+ finalize,
1806
+ expectState,
1807
+ range: { start, end: this.endPos() },
1808
+ leadingComments: leadingComments.length ? leadingComments : undefined
1809
+ }
1810
+ }
1811
+
1812
+ // ────────────────────────────────────────────────────────────────────
1813
+ // KML — retention and removal
1814
+ // ────────────────────────────────────────────────────────────────────
1815
+
1816
+ private parseSetRetention(): SetRetentionStatement {
1817
+ const leadingComments = this.collectLeadingComments()
1818
+ const start = this.currentPos()
1819
+ const set = this.expectKeywordWithSpace(TokenType.Set)
1820
+ this.expectSecondWord(TokenType.Retention, set)
1821
+ const target = this.parseTargetRef()
1822
+ const assignments = this.parseAssignmentObject()
1823
+
1824
+ let where: WhereClause | undefined
1825
+ if (this.check(TokenType.Where)) {
1826
+ this.expectKeywordWithSpace(TokenType.Where)
1827
+ this.dialect = 'raw'
1828
+ where = this.parseWhereClause()
1829
+ }
1830
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1831
+ const expectVersion = this.check(TokenType.Expect)
1832
+ ? this.parseExpectVersionClause()
1833
+ : undefined
1834
+
1835
+ return {
1836
+ kind: 'SetRetentionStatement',
1837
+ target,
1838
+ assignments,
1839
+ where,
1840
+ limit,
1841
+ expectVersion,
1842
+ range: { start, end: this.endPos() },
1843
+ leadingComments: leadingComments.length ? leadingComments : undefined
1844
+ }
1845
+ }
1846
+
1847
+ private parseArchiveStatement(): ArchiveStatement {
1848
+ const { start, target, where, limit, expectState, leadingComments } =
1849
+ this.parseRemovalBody(TokenType.Archive)
1850
+ return {
1851
+ kind: 'ArchiveStatement',
1852
+ target,
1853
+ where,
1854
+ limit,
1855
+ expectState,
1856
+ range: { start, end: this.endPos() },
1857
+ leadingComments
1858
+ }
1859
+ }
1860
+
1861
+ private parseTombstoneStatement(): TombstoneStatement {
1862
+ const { start, target, where, limit, expectState, leadingComments } =
1863
+ this.parseRemovalBody(TokenType.Tombstone)
1864
+ return {
1865
+ kind: 'TombstoneStatement',
1866
+ target,
1867
+ where,
1868
+ limit,
1869
+ expectState,
1870
+ range: { start, end: this.endPos() },
1871
+ leadingComments
1872
+ }
1873
+ }
1874
+
1875
+ /** ARCHIVE and TOMBSTONE share one shape; PURGE adds its confirmation. */
1876
+ private parseRemovalBody(keyword: TokenType): {
1877
+ start: Position
1878
+ target: TargetRef
1879
+ where?: WhereClause
1880
+ limit?: LimitClause
1881
+ expectState?: ExpectStateClause
1882
+ leadingComments?: string[]
1883
+ } {
1884
+ const comments = this.collectLeadingComments()
1885
+ const start = this.currentPos()
1886
+ this.expectKeywordWithSpace(keyword)
1887
+ const target = this.parseTargetRef()
1888
+
1889
+ let where: WhereClause | undefined
1890
+ if (this.check(TokenType.Where)) {
1891
+ this.expectKeywordWithSpace(TokenType.Where)
1892
+ this.dialect = 'raw'
1893
+ where = this.parseWhereClause()
1894
+ }
1895
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1896
+ const expectState = this.check(TokenType.Expect)
1897
+ ? this.parseExpectStateClause()
1898
+ : undefined
1899
+
1900
+ return {
1901
+ start,
1902
+ target,
1903
+ where,
1904
+ limit,
1905
+ expectState,
1906
+ leadingComments: comments.length ? comments : undefined
1907
+ }
1908
+ }
1909
+
1910
+ private parsePurgeStatement(): PurgeStatement {
1911
+ const leadingComments = this.collectLeadingComments()
1912
+ const start = this.currentPos()
1913
+ this.expectKeywordWithSpace(TokenType.Purge)
1914
+ const target = this.parseTargetRef()
1915
+
1916
+ let where: WhereClause | undefined
1917
+ if (this.check(TokenType.Where)) {
1918
+ this.expectKeywordWithSpace(TokenType.Where)
1919
+ this.dialect = 'raw'
1920
+ where = this.parseWhereClause()
1921
+ }
1922
+
1923
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1924
+
1925
+ let referencePolicy: ScalarValue | undefined
1926
+ if (this.check(TokenType.Reference)) {
1927
+ const reference = this.expect(TokenType.Reference)
1928
+ this.expectSecondWord(TokenType.Policy, reference)
1929
+ referencePolicy = this.parseScalarValue()
1930
+ }
1931
+
1932
+ // The grammar freezes the confirmation spelling. Physical erasure is
1933
+ // exceptional, so the literal is required and checked here rather than
1934
+ // left for the engine to discover.
1935
+ this.expect(TokenType.Confirm)
1936
+ const confirmTok = this.current()
1937
+ const confirm = this.parseStringLiteral()
1938
+ if (confirm.parsed !== 'PURGE') {
1939
+ this.error(
1940
+ `PURGE must be confirmed with the exact literal "PURGE", got ${confirmTok.value}`,
1941
+ confirmTok
1942
+ )
1943
+ }
1944
+
1945
+ return {
1946
+ kind: 'PurgeStatement',
1947
+ target,
1948
+ where,
1949
+ limit,
1950
+ referencePolicy,
1951
+ confirm,
1952
+ range: { start, end: this.endPos() },
1953
+ leadingComments: leadingComments.length ? leadingComments : undefined
1954
+ }
1955
+ }
1956
+
1957
+ private parseMergeConcept(): MergeConceptStatement {
1958
+ const leadingComments = this.collectLeadingComments()
1959
+ const start = this.currentPos()
1960
+ const merge = this.expectKeywordWithSpace(TokenType.Merge)
1961
+ this.expectSecondWord(TokenType.Concept, merge)
1962
+ const source = this.parseTargetRef()
1963
+ this.expect(TokenType.Into)
1964
+ const into = this.parseTargetRef()
1965
+
1966
+ let where: WhereClause | undefined
1967
+ if (this.check(TokenType.Where)) {
1968
+ this.expectKeywordWithSpace(TokenType.Where)
1969
+ this.dialect = 'raw'
1970
+ where = this.parseWhereClause()
1971
+ }
1972
+ const expectVersion = this.check(TokenType.Expect)
1973
+ ? this.parseExpectVersionClause()
1974
+ : undefined
1975
+
1976
+ return {
1977
+ kind: 'MergeConceptStatement',
1978
+ source,
1979
+ into,
1980
+ where,
1981
+ expectVersion,
1982
+ range: { start, end: this.endPos() },
1983
+ leadingComments: leadingComments.length ? leadingComments : undefined
1984
+ }
1985
+ }
1986
+
1987
+ // ────────────────────────────────────────────────────────────────────
1988
+ // META — DESCRIBE
1989
+ // ────────────────────────────────────────────────────────────────────
1990
+
1991
+ private parseDescribeStatement(): DescribeStatement {
1992
+ const leadingComments = this.collectLeadingComments()
1993
+ const start = this.currentPos()
1994
+ const describe = this.expectKeywordWithSpace(TokenType.Describe)
1995
+ const tok = this.current()
1996
+
1997
+ const stmt = (
1998
+ target: DescribeTargetKind,
1999
+ extra: Partial<DescribeStatement> = {}
2000
+ ): DescribeStatement => ({
2001
+ kind: 'DescribeStatement',
2002
+ target,
2003
+ ...extra,
2004
+ range: { start, end: this.endPos() },
2005
+ leadingComments: leadingComments.length ? leadingComments : undefined
2006
+ })
2007
+
2008
+ switch (tok.type) {
2009
+ case TokenType.Primer: {
2010
+ this.expectSecondWord(TokenType.Primer, describe)
2011
+ let mode: ScalarValue | undefined
2012
+ if (this.match(TokenType.Mode)) mode = this.parseScalarValue()
2013
+ return stmt('PRIMER', { mode })
2014
+ }
2015
+ case TokenType.Protocol:
2016
+ this.expectSecondWord(TokenType.Protocol, describe)
2017
+ return stmt('PROTOCOL')
2018
+ case TokenType.Execution: {
2019
+ const exec = this.expectSecondWord(TokenType.Execution, describe)
2020
+ this.expectSecondWord(TokenType.Context, exec)
2021
+ return stmt('EXECUTION_CONTEXT')
2022
+ }
2023
+ case TokenType.Capabilities:
2024
+ this.expectSecondWord(TokenType.Capabilities, describe)
2025
+ return stmt('CAPABILITIES')
2026
+ case TokenType.Space: {
2027
+ this.expectSecondWord(TokenType.Space, describe)
2028
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined
2029
+ return stmt('SPACE', { value })
2030
+ }
2031
+ case TokenType.Schema: {
2032
+ const schema = this.expectSecondWord(TokenType.Schema, describe)
2033
+ this.expectSecondWord(TokenType.Environment, schema)
2034
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined
2035
+ return stmt('SCHEMA_ENVIRONMENT', { asOf })
2036
+ }
2037
+ case TokenType.Package:
2038
+ this.expectSecondWord(TokenType.Package, describe)
2039
+ return stmt('PACKAGE', { value: this.parseScalarValue() })
2040
+ case TokenType.Type:
2041
+ this.expectSecondWord(TokenType.Type, describe)
2042
+ return stmt('TYPE', { value: this.parseScalarValue() })
2043
+ case TokenType.Predicate:
2044
+ this.expectSecondWord(TokenType.Predicate, describe)
2045
+ return stmt('PREDICATE', { value: this.parseScalarValue() })
2046
+ case TokenType.Facet:
2047
+ this.expectSecondWord(TokenType.Facet, describe)
2048
+ return stmt('FACET', { value: this.parseScalarValue() })
2049
+ case TokenType.Structural: {
2050
+ const structural = this.expectSecondWord(TokenType.Structural, describe)
2051
+ this.expectSecondWord(TokenType.Field, structural)
2052
+ return stmt('STRUCTURAL_FIELD', { value: this.parseScalarValue() })
2053
+ }
2054
+ case TokenType.Compatibility: {
2055
+ this.expectSecondWord(TokenType.Compatibility, describe)
2056
+ this.expect(TokenType.From)
2057
+ const from = this.parseScalarValue()
2058
+ this.expect(TokenType.To)
2059
+ const to = this.parseScalarValue()
2060
+ return stmt('COMPATIBILITY', { from, to })
2061
+ }
2062
+ case TokenType.Error:
2063
+ this.expectSecondWord(TokenType.Error, describe)
2064
+ return stmt('ERROR', { value: this.parseScalarValue() })
2065
+ case TokenType.Transaction: {
2066
+ const transaction = this.expectSecondWord(TokenType.Transaction, describe)
2067
+ if (this.check(TokenType.By)) {
2068
+ const by = this.expectSecondWord(TokenType.By, transaction)
2069
+ const idem = this.expectSecondWord(TokenType.Idempotency, by)
2070
+ this.expectSecondWord(TokenType.Key, idem)
2071
+ return stmt('TRANSACTION_BY_IDEMPOTENCY_KEY', {
2072
+ value: this.parseScalarValue()
2073
+ })
2074
+ }
2075
+ return stmt('TRANSACTION', { value: this.parseScalarValue() })
2076
+ }
2077
+ case TokenType.Snapshot: {
2078
+ this.expectSecondWord(TokenType.Snapshot, describe)
2079
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined
2080
+ return stmt('SNAPSHOT', { asOf })
2081
+ }
2082
+ case TokenType.Capsule:
2083
+ this.expectSecondWord(TokenType.Capsule, describe)
2084
+ return stmt('CAPSULE', { value: this.parseScalarValue() })
2085
+ case TokenType.Epistemic: {
2086
+ const epistemic = this.expectSecondWord(TokenType.Epistemic, describe)
2087
+ this.expectSecondWord(TokenType.Policy, epistemic)
2088
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined
2089
+ return stmt('EPISTEMIC_POLICY', { value })
2090
+ }
2091
+ case TokenType.Projection: {
2092
+ const projection = this.expectSecondWord(TokenType.Projection, describe)
2093
+ this.expectSecondWord(TokenType.Capability, projection)
2094
+ return stmt('PROJECTION_CAPABILITY')
2095
+ }
2096
+ case TokenType.Trust: {
2097
+ this.expectSecondWord(TokenType.Trust, describe)
2098
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined
2099
+ return stmt('TRUST', { value })
2100
+ }
2101
+ case TokenType.Access: {
2102
+ this.expectSecondWord(TokenType.Access, describe)
2103
+ const withOptions = this.match(TokenType.With)
2104
+ ? this.parseObjectLiteral()
2105
+ : undefined
2106
+ return stmt('ACCESS', { with: withOptions })
2107
+ }
2108
+ default:
2109
+ this.error(`Unknown DESCRIBE target '${tok.value}'`, tok)
2110
+ throw new ParseAbort()
2111
+ }
2112
+ }
2113
+
2114
+ // ────────────────────────────────────────────────────────────────────
2115
+ // META — LIST
2116
+ // ────────────────────────────────────────────────────────────────────
2117
+
2118
+ private parseListStatement(): ListStatement {
2119
+ const leadingComments = this.collectLeadingComments()
2120
+ const start = this.currentPos()
2121
+ const list = this.expectKeywordWithSpace(TokenType.List)
2122
+ const tok = this.current()
2123
+
2124
+ let target: ListTargetKind
2125
+ let status: ScalarValue | undefined
2126
+
2127
+ switch (tok.type) {
2128
+ case TokenType.Spaces:
2129
+ this.expectSecondWord(TokenType.Spaces, list)
2130
+ target = 'SPACES'
2131
+ break
2132
+ case TokenType.Schema: {
2133
+ const schema = this.expectSecondWord(TokenType.Schema, list)
2134
+ this.expectSecondWord(TokenType.Packages, schema)
2135
+ target = 'SCHEMA_PACKAGES'
2136
+ if (this.match(TokenType.Status)) status = this.parseScalarValue()
2137
+ break
2138
+ }
2139
+ case TokenType.Types:
2140
+ this.expectSecondWord(TokenType.Types, list)
2141
+ target = 'TYPES'
2142
+ break
2143
+ case TokenType.Predicates:
2144
+ this.expectSecondWord(TokenType.Predicates, list)
2145
+ target = 'PREDICATES'
2146
+ break
2147
+ case TokenType.Facets:
2148
+ this.expectSecondWord(TokenType.Facets, list)
2149
+ target = 'FACETS'
2150
+ break
2151
+ case TokenType.Structural: {
2152
+ const structural = this.expectSecondWord(TokenType.Structural, list)
2153
+ this.expectSecondWord(TokenType.Fields, structural)
2154
+ target = 'STRUCTURAL_FIELDS'
2155
+ break
2156
+ }
2157
+ case TokenType.Epistemic: {
2158
+ const epistemic = this.expectSecondWord(TokenType.Epistemic, list)
2159
+ this.expectSecondWord(TokenType.Policies, epistemic)
2160
+ target = 'EPISTEMIC_POLICIES'
2161
+ break
2162
+ }
2163
+ default:
2164
+ this.error(`Unknown LIST target '${tok.value}'`, tok)
2165
+ throw new ParseAbort()
2166
+ }
2167
+
2168
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
2169
+ const cursor = this.check(TokenType.Cursor)
2170
+ ? this.parseCursorClause()
2171
+ : undefined
2172
+
2173
+ return {
2174
+ kind: 'ListStatement',
2175
+ target,
2176
+ status,
2177
+ limit,
2178
+ cursor,
2179
+ range: { start, end: this.endPos() },
2180
+ leadingComments: leadingComments.length ? leadingComments : undefined
2181
+ }
2182
+ }
2183
+
2184
+ // ────────────────────────────────────────────────────────────────────
2185
+ // META — SEARCH
2186
+ // ────────────────────────────────────────────────────────────────────
2187
+
2188
+ private parseSearchStatement(): SearchStatement {
2189
+ const leadingComments = this.collectLeadingComments()
2190
+ const start = this.currentPos()
2191
+ const search = this.expectKeywordWithSpace(TokenType.Search)
2192
+
2193
+ const kindTok = this.current()
2194
+ let searchKind: SearchKind
2195
+ switch (kindTok.type) {
2196
+ case TokenType.Concept:
2197
+ searchKind = 'CONCEPT'
2198
+ break
2199
+ case TokenType.Proposition:
2200
+ searchKind = 'PROPOSITION'
2201
+ break
2202
+ case TokenType.Assertion:
2203
+ searchKind = 'ASSERTION'
2204
+ break
2205
+ case TokenType.Evidence:
2206
+ searchKind = 'EVIDENCE'
2207
+ break
2208
+ case TokenType.Activity:
2209
+ searchKind = 'ACTIVITY'
2210
+ break
2211
+ case TokenType.Cognition:
2212
+ searchKind = 'COGNITION'
2213
+ break
2214
+ default:
2215
+ this.error(
2216
+ `Expected CONCEPT, PROPOSITION, ASSERTION, EVIDENCE, ACTIVITY or COGNITION after SEARCH but got '${kindTok.value}'`,
2217
+ kindTok
2218
+ )
2219
+ throw new ParseAbort()
2220
+ }
2221
+ this.expectSecondWord(kindTok.type, search)
2222
+
2223
+ const term = this.parseScalarValue()
2224
+
2225
+ let withType: ScalarValue | undefined
2226
+ let withPredicate: ScalarValue | undefined
2227
+ let mode: ScalarValue | undefined
2228
+ let threshold: ScalarValue | undefined
2229
+ let asOfSeq: ScalarValue | undefined
2230
+
2231
+ // The grammar fixes this order; each modifier is taken at most once.
2232
+ while (this.check(TokenType.With)) {
2233
+ const withTok = this.expect(TokenType.With)
2234
+ if (this.check(TokenType.Type)) {
2235
+ this.expectSecondWord(TokenType.Type, withTok)
2236
+ this.rejectRepeat(withType, 'WITH TYPE', withTok)
2237
+ withType = this.parseScalarValue()
2238
+ } else if (this.check(TokenType.Predicate)) {
2239
+ this.expectSecondWord(TokenType.Predicate, withTok)
2240
+ this.rejectRepeat(withPredicate, 'WITH PREDICATE', withTok)
2241
+ withPredicate = this.parseScalarValue()
2242
+ } else {
2243
+ this.error(
2244
+ `Expected TYPE or PREDICATE after WITH but got '${this.current().value}'`,
2245
+ this.current()
2246
+ )
2247
+ break
2248
+ }
2249
+ }
2250
+
2251
+ if (this.match(TokenType.Mode)) mode = this.parseScalarValue()
2252
+ if (this.match(TokenType.Threshold)) threshold = this.parseScalarValue()
2253
+
2254
+ if (this.check(TokenType.As)) {
2255
+ const as = this.expect(TokenType.As)
2256
+ const of = this.expectSecondWord(TokenType.Of, as)
2257
+ this.expectSecondWord(TokenType.Seq, of)
2258
+ asOfSeq = this.parseScalarValue()
2259
+ }
2260
+
2261
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
2262
+ const cursor = this.check(TokenType.Cursor)
2263
+ ? this.parseCursorClause()
2264
+ : undefined
2265
+
2266
+ return {
2267
+ kind: 'SearchStatement',
2268
+ searchKind,
2269
+ term,
2270
+ withType,
2271
+ withPredicate,
2272
+ mode,
2273
+ threshold,
2274
+ asOfSeq,
2275
+ limit,
2276
+ cursor,
2277
+ range: { start, end: this.endPos() },
2278
+ leadingComments: leadingComments.length ? leadingComments : undefined
2279
+ }
2280
+ }
2281
+
2282
+ // ────────────────────────────────────────────────────────────────────
2283
+ // META — VERIFY / VALIDATE / PREVIEW
2284
+ // ────────────────────────────────────────────────────────────────────
2285
+
2286
+ private parseVerifyStatement(): VerifyStatement {
2287
+ const leadingComments = this.collectLeadingComments()
2288
+ const start = this.currentPos()
2289
+ const verify = this.expectKeywordWithSpace(TokenType.Verify)
2290
+ const tok = this.current()
2291
+
2292
+ let target: VerifyTargetKind
2293
+ switch (tok.type) {
2294
+ case TokenType.Capsule:
2295
+ this.expectSecondWord(TokenType.Capsule, verify)
2296
+ target = 'CAPSULE'
2297
+ break
2298
+ case TokenType.Schema: {
2299
+ const schema = this.expectSecondWord(TokenType.Schema, verify)
2300
+ this.expectSecondWord(TokenType.Package, schema)
2301
+ target = 'SCHEMA_PACKAGE'
2302
+ break
2303
+ }
2304
+ case TokenType.Receipt:
2305
+ this.expectSecondWord(TokenType.Receipt, verify)
2306
+ target = 'RECEIPT'
2307
+ break
2308
+ case TokenType.Blob:
2309
+ this.expectSecondWord(TokenType.Blob, verify)
2310
+ target = 'BLOB'
2311
+ break
2312
+ case TokenType.Checkpoint:
2313
+ this.expectSecondWord(TokenType.Checkpoint, verify)
2314
+ target = 'CHECKPOINT'
2315
+ break
2316
+ default:
2317
+ this.error(`Unknown VERIFY target '${tok.value}'`, tok)
2318
+ throw new ParseAbort()
2319
+ }
2320
+
2321
+ return {
2322
+ kind: 'VerifyStatement',
2323
+ target,
2324
+ value: this.parseScalarValue(),
2325
+ range: { start, end: this.endPos() },
2326
+ leadingComments: leadingComments.length ? leadingComments : undefined
2327
+ }
2328
+ }
2329
+
2330
+ private parseValidateStatement(): ValidateStatement {
2331
+ const leadingComments = this.collectLeadingComments()
2332
+ const start = this.currentPos()
2333
+ const validate = this.expectKeywordWithSpace(TokenType.Validate)
2334
+ const tok = this.current()
2335
+
2336
+ let target: ValidateTargetKind
2337
+ switch (tok.type) {
2338
+ case TokenType.Kql:
2339
+ this.expectSecondWord(TokenType.Kql, validate)
2340
+ target = 'KQL'
2341
+ break
2342
+ case TokenType.Kml:
2343
+ this.expectSecondWord(TokenType.Kml, validate)
2344
+ target = 'KML'
2345
+ break
2346
+ case TokenType.Capsule:
2347
+ this.expectSecondWord(TokenType.Capsule, validate)
2348
+ target = 'CAPSULE'
2349
+ break
2350
+ case TokenType.Schema: {
2351
+ const schema = this.expectSecondWord(TokenType.Schema, validate)
2352
+ this.expectSecondWord(TokenType.Package, schema)
2353
+ target = 'SCHEMA_PACKAGE'
2354
+ break
2355
+ }
2356
+ case TokenType.Import: {
2357
+ const importTok = this.expectSecondWord(TokenType.Import, validate)
2358
+ this.expectSecondWord(TokenType.Plan, importTok)
2359
+ target = 'IMPORT_PLAN'
2360
+ break
2361
+ }
2362
+ default:
2363
+ this.error(`Unknown VALIDATE target '${tok.value}'`, tok)
2364
+ throw new ParseAbort()
2365
+ }
2366
+
2367
+ const value = this.parseScalarValue()
2368
+ const options = this.match(TokenType.With)
2369
+ ? this.parseObjectLiteral()
2370
+ : undefined
2371
+
2372
+ return {
2373
+ kind: 'ValidateStatement',
2374
+ target,
2375
+ value,
2376
+ options,
2377
+ range: { start, end: this.endPos() },
2378
+ leadingComments: leadingComments.length ? leadingComments : undefined
2379
+ }
2380
+ }
2381
+
2382
+ private parsePreviewStatement(): PreviewStatement {
2383
+ const leadingComments = this.collectLeadingComments()
2384
+ const start = this.currentPos()
2385
+ const preview = this.expectKeywordWithSpace(TokenType.Preview)
2386
+ const tok = this.current()
2387
+
2388
+ if (tok.type === TokenType.Kml) {
2389
+ this.expectSecondWord(TokenType.Kml, preview)
2390
+ return {
2391
+ kind: 'PreviewStatement',
2392
+ target: 'KML',
2393
+ value: this.parseScalarValue(),
2394
+ range: { start, end: this.endPos() },
2395
+ leadingComments: leadingComments.length ? leadingComments : undefined
2396
+ }
2397
+ }
2398
+ if (tok.type === TokenType.Import) {
2399
+ const importTok = this.expectSecondWord(TokenType.Import, preview)
2400
+ this.expectSecondWord(TokenType.Capsule, importTok)
2401
+ const value = this.parseScalarValue()
2402
+ this.expect(TokenType.Into)
2403
+ const into = this.parseScalarValue()
2404
+ return {
2405
+ kind: 'PreviewStatement',
2406
+ target: 'IMPORT_CAPSULE',
2407
+ value,
2408
+ into,
2409
+ range: { start, end: this.endPos() },
2410
+ leadingComments: leadingComments.length ? leadingComments : undefined
2411
+ }
2412
+ }
2413
+
2414
+ this.error(
2415
+ `Expected KML or IMPORT CAPSULE after PREVIEW but got '${tok.value}'`,
2416
+ tok
2417
+ )
2418
+ throw new ParseAbort()
2419
+ }
2420
+
2421
+ // ────────────────────────────────────────────────────────────────────
2422
+ // META — HISTORY / CHANGES / SNAPSHOT
2423
+ // ────────────────────────────────────────────────────────────────────
2424
+
2425
+ private parseHistoryStatement(): HistoryStatement {
2426
+ const leadingComments = this.collectLeadingComments()
2427
+ const start = this.currentPos()
2428
+ const history = this.expectKeywordWithSpace(TokenType.History)
2429
+ const tok = this.current()
2430
+
2431
+ let target: 'ELEMENT' | 'SPACE'
2432
+ let value: ScalarValue | undefined
2433
+
2434
+ if (tok.type === TokenType.Element) {
2435
+ this.expectSecondWord(TokenType.Element, history)
2436
+ target = 'ELEMENT'
2437
+ value = this.parseScalarValue()
2438
+ } else if (tok.type === TokenType.Space) {
2439
+ this.expectSecondWord(TokenType.Space, history)
2440
+ target = 'SPACE'
2441
+ } else {
2442
+ this.error(
2443
+ `Expected ELEMENT or SPACE after HISTORY but got '${tok.value}'`,
2444
+ tok
2445
+ )
2446
+ throw new ParseAbort()
2447
+ }
2448
+
2449
+ let fromSeq: ScalarValue | undefined
2450
+ let toSeq: ScalarValue | undefined
2451
+ if (this.check(TokenType.From)) {
2452
+ const from = this.expect(TokenType.From)
2453
+ this.expectSecondWord(TokenType.Seq, from)
2454
+ fromSeq = this.parseScalarValue()
2455
+ }
2456
+ if (this.check(TokenType.To)) {
2457
+ const to = this.expect(TokenType.To)
2458
+ this.expectSecondWord(TokenType.Seq, to)
2459
+ toSeq = this.parseScalarValue()
2460
+ }
2461
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
2462
+ const cursor = this.check(TokenType.Cursor)
2463
+ ? this.parseCursorClause()
2464
+ : undefined
2465
+
2466
+ return {
2467
+ kind: 'HistoryStatement',
2468
+ target,
2469
+ value,
2470
+ fromSeq,
2471
+ toSeq,
2472
+ limit,
2473
+ cursor,
2474
+ range: { start, end: this.endPos() },
2475
+ leadingComments: leadingComments.length ? leadingComments : undefined
2476
+ }
2477
+ }
2478
+
2479
+ private parseChangesStatement(): ChangesStatement {
2480
+ const leadingComments = this.collectLeadingComments()
2481
+ const start = this.currentPos()
2482
+ const changes = this.expectKeywordWithSpace(TokenType.Changes)
2483
+
2484
+ let mode: 'SINCE' | 'AFTER_SEQ'
2485
+ if (this.check(TokenType.Since)) {
2486
+ this.expectSecondWord(TokenType.Since, changes)
2487
+ mode = 'SINCE'
2488
+ } else if (this.check(TokenType.After)) {
2489
+ const after = this.expectSecondWord(TokenType.After, changes)
2490
+ this.expectSecondWord(TokenType.Seq, after)
2491
+ mode = 'AFTER_SEQ'
2492
+ } else {
2493
+ this.error(
2494
+ `Expected SINCE or AFTER SEQ after CHANGES but got '${this.current().value}'`,
2495
+ this.current()
2496
+ )
2497
+ throw new ParseAbort()
2498
+ }
2499
+
2500
+ const value = this.parseScalarValue()
2501
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
2502
+
2503
+ return {
2504
+ kind: 'ChangesStatement',
2505
+ mode,
2506
+ value,
2507
+ limit,
2508
+ range: { start, end: this.endPos() },
2509
+ leadingComments: leadingComments.length ? leadingComments : undefined
2510
+ }
2511
+ }
2512
+
2513
+ private parseSnapshotStatement(): SnapshotStatement {
2514
+ const leadingComments = this.collectLeadingComments()
2515
+ const start = this.currentPos()
2516
+ this.expect(TokenType.Snapshot)
2517
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined
2518
+ return {
2519
+ kind: 'SnapshotStatement',
2520
+ asOf,
2521
+ range: { start, end: this.endPos() },
2522
+ leadingComments: leadingComments.length ? leadingComments : undefined
2523
+ }
2524
+ }
2525
+
2526
+ // ────────────────────────────────────────────────────────────────────
2527
+ // META — EXPORT CAPSULE
2528
+ // ────────────────────────────────────────────────────────────────────
2529
+
2530
+ private parseExportCapsuleStatement(): ExportCapsuleStatement {
2531
+ const leadingComments = this.collectLeadingComments()
2532
+ const start = this.currentPos()
2533
+ const exportTok = this.expectKeywordWithSpace(TokenType.Export)
2534
+ this.expectSecondWord(TokenType.Capsule, exportTok)
2535
+ const target = this.parseTargetRef()
2536
+
2537
+ this.expectKeywordWithSpace(TokenType.Where)
2538
+ // A capsule carries records, not interpretations: BELIEF is excluded.
2539
+ this.dialect = 'raw'
2540
+ const where = this.parseWhereClause()
2541
+
2542
+ const options = this.match(TokenType.With)
2543
+ ? this.parseObjectLiteral()
2544
+ : undefined
2545
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined
2546
+
2547
+ return {
2548
+ kind: 'ExportCapsuleStatement',
2549
+ target,
2550
+ where,
2551
+ options,
2552
+ asOf,
2553
+ range: { start, end: this.endPos() },
2554
+ leadingComments: leadingComments.length ? leadingComments : undefined
2555
+ }
2556
+ }
2557
+
2558
+ // ────────────────────────────────────────────────────────────────────
2559
+ // Expressions
2560
+ // ────────────────────────────────────────────────────────────────────
2561
+
2562
+ private parseExpression(): Expression {
2563
+ return this.parseOrExpression()
2564
+ }
2565
+
2566
+ private parseOrExpression(): Expression {
2567
+ let left = this.parseAndExpression()
2568
+ while (this.check(TokenType.Or)) {
2569
+ const start = left.range.start
2570
+ this.advance()
2571
+ const right = this.parseAndExpression()
2572
+ left = {
2573
+ kind: 'BinaryExpression',
2574
+ operator: '||',
2575
+ left,
2576
+ right,
2577
+ range: { start, end: this.endPos() }
2578
+ }
2579
+ }
2580
+ return left
2581
+ }
2582
+
2583
+ private parseAndExpression(): Expression {
2584
+ let left = this.parseEqualityExpression()
2585
+ while (this.check(TokenType.And)) {
2586
+ const start = left.range.start
2587
+ this.advance()
2588
+ const right = this.parseEqualityExpression()
2589
+ left = {
2590
+ kind: 'BinaryExpression',
2591
+ operator: '&&',
2592
+ left,
2593
+ right,
2594
+ range: { start, end: this.endPos() }
2595
+ }
2596
+ }
2597
+ return left
2598
+ }
2599
+
2600
+ private parseEqualityExpression(): Expression {
2601
+ let left = this.parseRelationalExpression()
2602
+ while (this.check(TokenType.Eq) || this.check(TokenType.NotEq)) {
2603
+ const start = left.range.start
2604
+ const op = this.advance().type
2605
+ const right = this.parseRelationalExpression()
2606
+ left = {
2607
+ kind: 'BinaryExpression',
2608
+ operator: op === TokenType.Eq ? '==' : '!=',
2609
+ left,
2610
+ right,
2611
+ range: { start, end: this.endPos() }
2612
+ }
2613
+ }
2614
+ return left
2615
+ }
2616
+
2617
+ /**
2618
+ * `relational_expression` takes at most one comparison.
2619
+ *
2620
+ * `a < b < c` is not chained comparison in KIP, it is a grammar error, and
2621
+ * accepting it here would give the parser a meaning the reference grammar
2622
+ * does not define.
2623
+ */
2624
+ private parseRelationalExpression(): Expression {
2625
+ const left = this.parseUnaryExpression()
2626
+ const tok = this.current()
2627
+ let operator: string | undefined
2628
+ switch (tok.type) {
2629
+ case TokenType.Lt:
2630
+ operator = '<'
2631
+ break
2632
+ case TokenType.Gt:
2633
+ operator = '>'
2634
+ break
2635
+ case TokenType.LtEq:
2636
+ operator = '<='
2637
+ break
2638
+ case TokenType.GtEq:
2639
+ operator = '>='
2640
+ break
2641
+ default:
2642
+ return left
2643
+ }
2644
+ this.advance()
2645
+ const right = this.parseUnaryExpression()
2646
+ const result: Expression = {
2647
+ kind: 'BinaryExpression',
2648
+ operator,
2649
+ left,
2650
+ right,
2651
+ range: { start: left.range.start, end: this.endPos() }
2652
+ }
2653
+ const next = this.current()
2654
+ if (
2655
+ next.type === TokenType.Lt ||
2656
+ next.type === TokenType.Gt ||
2657
+ next.type === TokenType.LtEq ||
2658
+ next.type === TokenType.GtEq
2659
+ ) {
2660
+ this.error(
2661
+ `Chained comparison '${next.value}' is not allowed; use && between comparisons`,
2662
+ next
2663
+ )
2664
+ }
2665
+ return result
2666
+ }
2667
+
2668
+ private parseUnaryExpression(): Expression {
2669
+ const tok = this.current()
2670
+ if (tok.type === TokenType.Bang || tok.type === TokenType.Minus) {
2671
+ const start = this.currentPos()
2672
+ this.advance()
2673
+ const operand = this.parsePrimaryExpression()
2674
+ return {
2675
+ kind: 'UnaryExpression',
2676
+ operator: tok.type === TokenType.Bang ? '!' : '-',
2677
+ operand,
2678
+ range: { start, end: this.endPos() }
2679
+ }
2680
+ }
2681
+ return this.parsePrimaryExpression()
2682
+ }
2683
+
2684
+ private parsePrimaryExpression(): Expression {
2685
+ const tok = this.current()
2686
+
2687
+ switch (tok.type) {
2688
+ case TokenType.Variable:
2689
+ return this.parseFieldAccessOrVariable()
2690
+ case TokenType.Parameter:
2691
+ return this.parseParameterRef()
2692
+ case TokenType.String:
2693
+ case TokenType.Number:
2694
+ case TokenType.Boolean:
2695
+ case TokenType.Null:
2696
+ return this.parseLiteral()
2697
+ case TokenType.LBracket:
2698
+ return this.parseArrayLiteral()
2699
+ case TokenType.LBrace:
2700
+ return this.parseObjectLiteral()
2701
+ case TokenType.LParen: {
2702
+ this.advance()
2703
+ const inner = this.parseExpression()
2704
+ this.expect(TokenType.RParen)
2705
+ return inner
2706
+ }
2707
+ default:
2708
+ // `function_call` is an open `identifier "("`, and KIP 2.0 keywords
2709
+ // are contextual, so any identifier-like token may name a function.
2710
+ if (isIdentifierLike(tok.type) && this.peekPast(1)?.type === TokenType.LParen) {
2711
+ return this.parseCallExpression()
2712
+ }
2713
+ this.error(`Unexpected token '${tok.value}' in expression`, tok)
2714
+ this.advance()
2715
+ return {
2716
+ kind: 'NullLiteral',
2717
+ range: { start: this.currentPos(), end: this.endPos() }
2718
+ }
2719
+ }
2720
+ }
2721
+
2722
+ /** `COUNT(DISTINCT ?x)` where the name is an aggregate, else a plain call. */
2723
+ private parseCallExpression(): FunctionCallExpr | AggregateExpr {
2724
+ const start = this.currentPos()
2725
+ const nameTok = this.advance()
2726
+ const name = nameTok.value
2727
+ this.expect(TokenType.LParen)
2728
+
2729
+ if (isAggregate(name)) {
2730
+ const distinct = this.match(TokenType.Distinct)
2731
+ const argument = this.parseExpression()
2732
+ this.expect(TokenType.RParen)
2733
+ return {
2734
+ kind: 'AggregateExpr',
2735
+ name: name.toUpperCase(),
2736
+ distinct,
2737
+ argument,
2738
+ range: { start, end: this.endPos() }
2739
+ }
2740
+ }
2741
+
2742
+ const args: Expression[] = []
2743
+ if (!this.check(TokenType.RParen)) {
2744
+ do {
2745
+ args.push(this.parseExpression())
2746
+ } while (this.match(TokenType.Comma))
2747
+ }
2748
+ this.expect(TokenType.RParen)
2749
+ return {
2750
+ kind: 'FunctionCallExpr',
2751
+ name,
2752
+ args,
2753
+ range: { start, end: this.endPos() }
2754
+ }
2755
+ }
2756
+
2757
+ /**
2758
+ * `field_access = variable, { field_step }`.
2759
+ *
2760
+ * A dot path carries no whitespace: `?x . name` is three tokens to a
2761
+ * conformant engine, not one path, so the gap is checked here.
2762
+ */
2763
+ private parseFieldAccessOrVariable(): VariableRef | FieldAccess {
2764
+ const base = this.parseVariableRef()
2765
+ if (!this.isTightFieldStepStart()) return base
2766
+
2767
+ const steps: FieldStep[] = []
2768
+ while (this.isTightFieldStepStart()) {
2769
+ const start = this.currentPos()
2770
+ if (this.check(TokenType.Dot)) {
2771
+ this.advance()
2772
+ const nameTok = this.current()
2773
+ if (!isIdentifierLike(nameTok.type)) {
2774
+ this.error(
2775
+ `Expected a field name after '.' but got '${nameTok.value}'`,
2776
+ nameTok
2777
+ )
2778
+ break
2779
+ }
2780
+ this.advance()
2781
+ steps.push({
2782
+ kind: 'DotStep',
2783
+ name: nameTok.value,
2784
+ range: { start, end: this.endPos() }
2785
+ })
2786
+ } else {
2787
+ this.advance() // [
2788
+ const key = this.parseStringLiteral()
2789
+ this.expect(TokenType.RBracket)
2790
+ steps.push({
2791
+ kind: 'IndexStep',
2792
+ key,
2793
+ range: { start, end: this.endPos() }
2794
+ })
2795
+ }
2796
+ }
2797
+
2798
+ return {
2799
+ kind: 'FieldAccess',
2800
+ base,
2801
+ steps,
2802
+ range: { start: base.range.start, end: this.endPos() }
2803
+ }
2804
+ }
2805
+
2806
+ /** True when a `.`/`[` follows with no gap, i.e. continues the path. */
2807
+ private isTightFieldStepStart(): boolean {
2808
+ const tok = this.current()
2809
+ if (tok.type !== TokenType.Dot && tok.type !== TokenType.LBracket) {
2810
+ return false
2811
+ }
2812
+ const prev = this.tokens[this.pos - 1]
2813
+ if (!prev) return false
2814
+ return prev.offset + prev.value.length === tok.offset
2815
+ }
2816
+
2817
+ // ────────────────────────────────────────────────────────────────────
2818
+ // Values, objects, arrays
2819
+ // ────────────────────────────────────────────────────────────────────
2820
+
2821
+ private parseVariableRef(): VariableRef {
2822
+ const tok = this.current()
2823
+ if (tok.type !== TokenType.Variable) {
2824
+ this.error(`Expected a variable (e.g. ?name) but got '${tok.value}'`, tok)
2825
+ return {
2826
+ kind: 'VariableRef',
2827
+ name: '?unknown',
2828
+ range: { start: this.currentPos(), end: this.endPos() }
2829
+ }
2830
+ }
2831
+ const start = this.currentPos()
2832
+ this.advance()
2833
+ return {
2834
+ kind: 'VariableRef',
2835
+ name: tok.value,
2836
+ range: { start, end: this.endPos() }
2837
+ }
2838
+ }
2839
+
2840
+ private parseParameterRef(): ParameterRef {
2841
+ const tok = this.current()
2842
+ const start = this.currentPos()
2843
+ this.advance()
2844
+ return {
2845
+ kind: 'ParameterRef',
2846
+ name: tok.value,
2847
+ range: { start, end: this.endPos() }
2848
+ }
2849
+ }
2850
+
2851
+ private parseStringLiteral(): StringLiteral {
2852
+ const tok = this.current()
2853
+ if (tok.type !== TokenType.String) {
2854
+ this.error(`Expected a quoted string but got '${tok.value}'`, tok)
2855
+ return {
2856
+ kind: 'StringLiteral',
2857
+ value: '""',
2858
+ parsed: '',
2859
+ range: { start: this.currentPos(), end: this.endPos() }
2860
+ }
2861
+ }
2862
+ const start = this.currentPos()
2863
+ this.advance()
2864
+ return {
2865
+ kind: 'StringLiteral',
2866
+ value: tok.value,
2867
+ parsed: this.unescapeString(tok.value, tok),
2868
+ range: { start, end: this.endPos() }
2869
+ }
2870
+ }
2871
+
2872
+ private parseLiteral(): LiteralNode {
2873
+ const tok = this.current()
2874
+ const start = this.currentPos()
2875
+ switch (tok.type) {
2876
+ case TokenType.String:
2877
+ return this.parseStringLiteral()
2878
+ case TokenType.Number: {
2879
+ this.advance()
2880
+ const value = Number(tok.value)
2881
+ if (!Number.isFinite(value)) {
2882
+ this.error(
2883
+ `Only finite numbers are valid KIP literals, got '${tok.value}'`,
2884
+ tok
2885
+ )
2886
+ }
2887
+ return {
2888
+ kind: 'NumberLiteral',
2889
+ value,
2890
+ raw: tok.value,
2891
+ range: { start, end: this.endPos() }
2892
+ }
2893
+ }
2894
+ case TokenType.Boolean:
2895
+ this.advance()
2896
+ return {
2897
+ kind: 'BooleanLiteral',
2898
+ value: tok.value === 'true',
2899
+ range: { start, end: this.endPos() }
2900
+ }
2901
+ case TokenType.Null:
2902
+ this.advance()
2903
+ return { kind: 'NullLiteral', range: { start, end: this.endPos() } }
2904
+ default:
2905
+ this.error(`Expected a literal but got '${tok.value}'`, tok)
2906
+ this.advance()
2907
+ return { kind: 'NullLiteral', range: { start, end: this.endPos() } }
2908
+ }
2909
+ }
2910
+
2911
+ /** `scalar_value` / `meta_value` = `parameter | literal` */
2912
+ private parseScalarValue(): ScalarValue {
2913
+ const tok = this.current()
2914
+ if (tok.type === TokenType.Parameter) {
2915
+ return this.parseParameterRef()
2916
+ }
2917
+ if (
2918
+ tok.type === TokenType.String ||
2919
+ tok.type === TokenType.Number ||
2920
+ tok.type === TokenType.Boolean ||
2921
+ tok.type === TokenType.Null
2922
+ ) {
2923
+ return this.parseLiteral() as ScalarValue
2924
+ }
2925
+ this.error(
2926
+ `Expected a literal or :parameter but got '${tok.value}'`,
2927
+ tok
2928
+ )
2929
+ this.advance()
2930
+ return {
2931
+ kind: 'NullLiteral',
2932
+ range: { start: this.currentPos(), end: this.endPos() }
2933
+ }
2934
+ }
2935
+
2936
+ /** True when the next token could begin a `meta_value`. */
2937
+ private isMetaValueStart(): boolean {
2938
+ const t = this.current().type
2939
+ return (
2940
+ t === TokenType.Parameter ||
2941
+ t === TokenType.String ||
2942
+ t === TokenType.Number ||
2943
+ t === TokenType.Boolean ||
2944
+ t === TokenType.Null
2945
+ )
2946
+ }
2947
+
2948
+ /** `schema_symbol = string_literal | parameter` */
2949
+ private parseSchemaSymbol(): SchemaSymbol {
2950
+ const tok = this.current()
2951
+ if (tok.type === TokenType.Parameter) {
2952
+ return this.parseParameterRef()
2953
+ }
2954
+ if (tok.type === TokenType.String) {
2955
+ return this.parseStringLiteral()
2956
+ }
2957
+ this.error(
2958
+ `Expected a schema symbol (quoted name or :parameter) but got '${tok.value}'`,
2959
+ tok
2960
+ )
2961
+ this.advance()
2962
+ return {
2963
+ kind: 'StringLiteral',
2964
+ value: '""',
2965
+ parsed: '',
2966
+ range: { start: this.currentPos(), end: this.endPos() }
2967
+ }
2968
+ }
2969
+
2970
+ /** `target_ref = variable | parameter | string_literal` */
2971
+ private parseTargetRef(): TargetRef {
2972
+ const tok = this.current()
2973
+ if (tok.type === TokenType.Variable) return this.parseVariableRef()
2974
+ if (tok.type === TokenType.Parameter) return this.parseParameterRef()
2975
+ if (tok.type === TokenType.String) return this.parseStringLiteral()
2976
+ this.error(
2977
+ `Expected a target (?variable, :parameter or quoted id) but got '${tok.value}'`,
2978
+ tok
2979
+ )
2980
+ this.advance()
2981
+ return {
2982
+ kind: 'StringLiteral',
2983
+ value: '""',
2984
+ parsed: '',
2985
+ range: { start: this.currentPos(), end: this.endPos() }
2986
+ }
2987
+ }
2988
+
2989
+ private expectHandle(): VariableRef {
2990
+ const tok = this.current()
2991
+ if (tok.type !== TokenType.Variable) {
2992
+ this.error(
2993
+ `Expected a local handle (e.g. ?e) but got '${tok.value}'`,
2994
+ tok
2995
+ )
2996
+ return {
2997
+ kind: 'VariableRef',
2998
+ name: '?unknown',
2999
+ range: { start: this.currentPos(), end: this.endPos() }
3000
+ }
3001
+ }
3002
+ return this.parseVariableRef()
3003
+ }
3004
+
3005
+ /** `mutation_value` — everything a KML assignment may hold. */
3006
+ private parseMutationValue(): Expression {
3007
+ const tok = this.current()
3008
+ switch (tok.type) {
3009
+ case TokenType.Variable:
3010
+ return this.parseFieldAccessOrVariable()
3011
+ case TokenType.Parameter:
3012
+ return this.parseParameterRef()
3013
+ case TokenType.LBracket:
3014
+ return this.parseArrayLiteral()
3015
+ case TokenType.LBrace:
3016
+ return this.parseObjectLiteral()
3017
+ case TokenType.String:
3018
+ case TokenType.Number:
3019
+ case TokenType.Boolean:
3020
+ case TokenType.Null:
3021
+ return this.parseLiteral()
3022
+ default:
3023
+ if (isIdentifierLike(tok.type) && this.peekPast(1)?.type === TokenType.LParen) {
3024
+ return this.parseCallExpression()
3025
+ }
3026
+ this.error(`Unexpected token '${tok.value}' in assignment value`, tok)
3027
+ this.advance()
3028
+ return {
3029
+ kind: 'NullLiteral',
3030
+ range: { start: this.currentPos(), end: this.endPos() }
3031
+ }
3032
+ }
3033
+ }
3034
+
3035
+ /** `assignment_object = "{" [assignment_member {"," assignment_member}] "}"` */
3036
+ private parseAssignmentObject(): ObjectLiteral {
3037
+ const start = this.currentPos()
3038
+ this.expect(TokenType.LBrace)
3039
+ const seen = { trailingComma: false }
3040
+ const entries = this.parseEntries(seen, () => this.parseMutationValue())
3041
+ this.expect(TokenType.RBrace)
3042
+ return {
3043
+ kind: 'ObjectLiteral',
3044
+ entries,
3045
+ trailingComma: seen.trailingComma || undefined,
3046
+ range: { start, end: this.endPos() }
3047
+ }
3048
+ }
3049
+
3050
+ /** `object_pattern` — `{...}` in matching position. */
3051
+ private parseObjectPattern(): ObjectPattern {
3052
+ const start = this.currentPos()
3053
+ this.expect(TokenType.LBrace)
3054
+ const seen = { trailingComma: false }
3055
+ const members = this.parseEntries(seen, () => this.parsePatternValue())
3056
+ this.expect(TokenType.RBrace)
3057
+ return {
3058
+ kind: 'ObjectPattern',
3059
+ members,
3060
+ trailingComma: seen.trailingComma || undefined,
3061
+ range: { start, end: this.endPos() }
3062
+ }
3063
+ }
3064
+
3065
+ private parsePatternValue(): Expression {
3066
+ const tok = this.current()
3067
+ switch (tok.type) {
3068
+ case TokenType.Variable:
3069
+ return this.parseVariableRef()
3070
+ case TokenType.Parameter:
3071
+ return this.parseParameterRef()
3072
+ case TokenType.LBracket:
3073
+ return this.parseArrayPattern()
3074
+ case TokenType.LBrace:
3075
+ return this.parseObjectPattern()
3076
+ case TokenType.LParen:
3077
+ return this.parsePropositionTuple()
3078
+ case TokenType.String:
3079
+ case TokenType.Number:
3080
+ case TokenType.Boolean:
3081
+ case TokenType.Null:
3082
+ return this.parseLiteral()
3083
+ default:
3084
+ this.error(`Unexpected token '${tok.value}' in match pattern`, tok)
3085
+ this.advance()
3086
+ return {
3087
+ kind: 'NullLiteral',
3088
+ range: { start: this.currentPos(), end: this.endPos() }
3089
+ }
3090
+ }
3091
+ }
3092
+
3093
+ private parseArrayPattern(): ArrayLiteral {
3094
+ return this.parseArrayWith(() => this.parsePatternValue())
3095
+ }
3096
+
3097
+ private parseArrayLiteral(): ArrayLiteral {
3098
+ return this.parseArrayWith(() => this.parseExpression())
3099
+ }
3100
+
3101
+ private parseArrayWith(parseElement: () => Expression): ArrayLiteral {
3102
+ const start = this.currentPos()
3103
+ this.expect(TokenType.LBracket)
3104
+ const elements: Expression[] = []
3105
+ let trailingComma = false
3106
+ if (!this.check(TokenType.RBracket)) {
3107
+ do {
3108
+ if (this.check(TokenType.RBracket)) {
3109
+ trailingComma = true
3110
+ break
3111
+ }
3112
+ elements.push(parseElement())
3113
+ } while (this.match(TokenType.Comma))
3114
+ }
3115
+ this.expect(TokenType.RBracket)
3116
+ return {
3117
+ kind: 'ArrayLiteral',
3118
+ elements,
3119
+ trailingComma: trailingComma || undefined,
3120
+ range: { start, end: this.endPos() }
3121
+ }
3122
+ }
3123
+
3124
+ private parseObjectLiteral(): ObjectLiteral {
3125
+ const start = this.currentPos()
3126
+ this.expect(TokenType.LBrace)
3127
+ const seen = { trailingComma: false }
3128
+ const entries = this.parseEntries(seen, () => this.parseExpression())
3129
+ this.expect(TokenType.RBrace)
3130
+ return {
3131
+ kind: 'ObjectLiteral',
3132
+ entries,
3133
+ trailingComma: seen.trailingComma || undefined,
3134
+ range: { start, end: this.endPos() }
3135
+ }
3136
+ }
3137
+
3138
+ private parseEntries(
3139
+ seen: { trailingComma: boolean },
3140
+ parseValue: () => Expression
3141
+ ): ObjectEntry[] {
3142
+ const entries: ObjectEntry[] = []
3143
+ if (this.check(TokenType.RBrace)) return entries
3144
+
3145
+ do {
3146
+ this.skipComments()
3147
+ if (this.check(TokenType.RBrace)) {
3148
+ seen.trailingComma = entries.length > 0
3149
+ break
3150
+ }
3151
+ const start = this.currentPos()
3152
+ const { key, isQuoted } = this.expectKeyWithQuoting()
3153
+ this.expectObjectColon(key)
3154
+ const value = parseValue()
3155
+ entries.push({
3156
+ kind: 'ObjectEntry',
3157
+ key,
3158
+ isQuoted,
3159
+ value,
3160
+ range: { start, end: this.endPos() }
3161
+ })
3162
+ } while (this.match(TokenType.Comma))
3163
+
3164
+ return entries
3165
+ }
3166
+
3167
+ // ────────────────────────────────────────────────────────────────────
3168
+ // Token helpers
3169
+ // ────────────────────────────────────────────────────────────────────
3170
+
3171
+ private current(): Token {
3172
+ return (
3173
+ this.tokens[this.pos] ?? {
3174
+ type: TokenType.EOF,
3175
+ value: '',
3176
+ offset: this.source.length,
3177
+ line: 0,
3178
+ column: 0
3179
+ }
3180
+ )
3181
+ }
3182
+
3183
+ private currentPos(): Position {
3184
+ const tok = this.current()
3185
+ return { line: tok.line, column: tok.column }
3186
+ }
3187
+
3188
+ /**
3189
+ * The end of the most recently consumed token — where a node actually ends.
3190
+ *
3191
+ * `currentPos()` points at the *next* token, so using it as `range.end`
3192
+ * stretches every node to the start of whatever follows. An editor folding
3193
+ * on that range would hide the first line of the next clause, so the end is
3194
+ * measured from the last token the node consumed. Comments are skipped
3195
+ * because `advance()` steps over them without them belonging to the node.
3196
+ */
3197
+ private endPos(): Position {
3198
+ let i = this.pos - 1
3199
+ while (i >= 0 && this.tokens[i]!.type === TokenType.Comment) i--
3200
+ const tok = this.tokens[i]
3201
+ if (!tok) return this.currentPos()
3202
+ return { line: tok.line, column: tok.column + tok.value.length }
3203
+ }
3204
+
3205
+ private isAtEnd(): boolean {
3206
+ return (
3207
+ this.pos >= this.tokens.length || this.current().type === TokenType.EOF
3208
+ )
3209
+ }
3210
+
3211
+ private check(type: TokenType): boolean {
3212
+ return this.current().type === type
3213
+ }
3214
+
3215
+ private match(type: TokenType): boolean {
3216
+ if (this.check(type)) {
3217
+ this.advance()
3218
+ return true
3219
+ }
3220
+ return false
3221
+ }
3222
+
3223
+ private advance(): Token {
3224
+ const tok = this.current()
3225
+ if (!this.isAtEnd()) this.pos++
3226
+ this.skipComments()
3227
+ return tok
3228
+ }
3229
+
3230
+ /** The token `i` positions ahead, skipping nothing. */
3231
+ private peekPast(i: number): Token | undefined {
3232
+ return this.tokens[this.pos + i]
3233
+ }
3234
+
3235
+ private expect(type: TokenType): Token {
3236
+ const tok = this.current()
3237
+ if (tok.type !== type) {
3238
+ this.error(`Expected '${type}' but got '${tok.value}'`, tok)
3239
+ return tok
3240
+ }
3241
+ return this.advance()
3242
+ }
3243
+
3244
+ /**
3245
+ * Consumes a keyword that the grammar requires to be followed by whitespace.
3246
+ *
3247
+ * Most KIP keywords only need a word boundary, so `WHERE{...}` is legal.
3248
+ * A handful — the statement introducers and the clause keywords whose
3249
+ * operand may itself start with a brace or a quote — require real
3250
+ * whitespace, which is what keeps `MUTATE{` from reading as a statement.
3251
+ * The distinction is per-keyword-position, not per-keyword, so it lives at
3252
+ * the call site rather than in the lexer.
3253
+ */
3254
+ private expectKeywordWithSpace(type: TokenType): Token {
3255
+ const tok = this.current()
3256
+ if (tok.type !== type) {
3257
+ this.error(`Expected '${type}' but got '${tok.value}'`, tok)
3258
+ return tok
3259
+ }
3260
+ const after = this.source[tok.offset + tok.value.length] ?? ''
3261
+ if (after !== ' ' && after !== '\t' && after !== '\r' && after !== '\n') {
3262
+ this.error(`'${tok.value}' must be followed by whitespace`, tok, 'KIP_1001')
3263
+ }
3264
+ return this.advance()
3265
+ }
3266
+
3267
+ /**
3268
+ * Consumes the second word of a multi-word keyword (`SET FIELDS`,
3269
+ * `AS OF`, `EXPECT VERSION`, `BELIEF SLOT`, ...).
3270
+ *
3271
+ * The grammar joins these with whitespace only. A comment between the words
3272
+ * is not a smaller gap, it is a different token sequence, and reading
3273
+ * `SET//c\nFIELDS` as `SET FIELDS` would accept text the reference grammar
3274
+ * rejects.
3275
+ */
3276
+ private expectSecondWord(type: TokenType, first: Token): Token {
3277
+ const tok = this.current()
3278
+ const gap = this.source.slice(first.offset + first.value.length, tok.offset)
3279
+ if (tok.type === type && !/^\s+$/.test(gap)) {
3280
+ this.error(
3281
+ `'${first.value} ${tok.value}' must be separated by whitespace only`,
3282
+ tok
3283
+ )
3284
+ }
3285
+ return this.expect(type)
3286
+ }
3287
+
3288
+ private expectKeyWithQuoting(): { key: string; isQuoted: boolean } {
3289
+ const tok = this.current()
3290
+ if (tok.type === TokenType.String) {
3291
+ this.advance()
3292
+ return { key: this.unescapeString(tok.value, tok), isQuoted: true }
3293
+ }
3294
+ // `field_name = identifier | string_literal`, and KIP 2.0 keywords are
3295
+ // contextual: the Spec's own ASSERT sugar writes `by:`, `mode:`, `at:`
3296
+ // and `key:` as object keys.
3297
+ if (isIdentifierLike(tok.type)) {
3298
+ this.advance()
3299
+ return { key: tok.value, isQuoted: false }
3300
+ }
3301
+ this.error(`Expected object key but got '${tok.value}'`, tok)
3302
+ this.advance()
3303
+ return { key: tok.value, isQuoted: false }
3304
+ }
3305
+
3306
+ /** Reports a clause written twice in a statement that allows it once. */
3307
+ private rejectRepeat(seen: unknown, name: string, tok: Token): void {
3308
+ if (seen !== undefined) {
3309
+ this.error(`Duplicate ${name} clause`, tok)
3310
+ }
3311
+ }
3312
+
3313
+ /** Enforces the canonical statement-level order while still recovering. */
3314
+ private checkClauseOrder(
3315
+ order: number,
3316
+ previous: number,
3317
+ name: string,
3318
+ tok: Token
3319
+ ): number {
3320
+ if (order < previous) {
3321
+ this.error(`${name} clause is out of order`, tok)
3322
+ }
3323
+ return Math.max(order, previous)
3324
+ }
3325
+
3326
+ private skipComments(): void {
3327
+ while (
3328
+ this.pos < this.tokens.length &&
3329
+ this.current().type === TokenType.Comment
3330
+ ) {
3331
+ this.pos++
3332
+ }
3333
+ }
3334
+
3335
+ private collectLeadingComments(): string[] {
3336
+ const comments: string[] = []
3337
+ // Look backwards from current position to collect contiguous comment tokens
3338
+ let i = this.pos - 1
3339
+ while (i >= 0 && this.tokens[i]!.type === TokenType.Comment) {
3340
+ comments.unshift(this.tokens[i]!.value)
3341
+ i--
3342
+ }
3343
+ return comments
3344
+ }
3345
+
3346
+ /**
3347
+ * Consume the `:` separating an object key from its value. A colon written
3348
+ * with no space before an identifier value (e.g. `status:active`) is lexed as
3349
+ * a single parameter placeholder token (`:active`), so split it back apart.
3350
+ */
3351
+ private expectObjectColon(_key: string): void {
3352
+ if (this.check(TokenType.Colon)) {
3353
+ this.advance()
3354
+ return
3355
+ }
3356
+ // `{"a":true}` lexes as a key followed by the parameter `:true`, because
3357
+ // `:name` is the placeholder syntax and the lexer cannot see that this
3358
+ // colon separates a key from its value. In key position the separator
3359
+ // reading is the only valid one, so split the token back apart and re-lex
3360
+ // the tail as the value.
3361
+ const tok = this.current()
3362
+ if (tok.type === TokenType.Parameter) {
3363
+ this.splitParameterAfterColon(tok)
3364
+ return
3365
+ }
3366
+ this.expect(TokenType.Colon)
3367
+ }
3368
+
3369
+ /**
3370
+ * Rewrites a `:value` parameter token in separator position into the value
3371
+ * tokens it spells, so the parser sees `: value`.
3372
+ */
3373
+ private splitParameterAfterColon(tok: Token): void {
3374
+ const tail = tok.value.slice(1)
3375
+ const retoken = tokenize(tail)
3376
+ .filter((t) => !isTrivia(t.type) && t.type !== TokenType.EOF)
3377
+ .map((t) => ({
3378
+ ...t,
3379
+ offset: tok.offset + 1 + t.offset,
3380
+ line: tok.line,
3381
+ column: tok.column + 1 + t.column
3382
+ }))
3383
+ this.tokens.splice(this.pos, 1, ...retoken)
3384
+ }
3385
+
3386
+ /**
3387
+ * Reads the value of a string token.
3388
+ *
3389
+ * KIP strings are JSON strings, so `"a\xb"` and an unterminated literal are
3390
+ * both errors — but an editor still wants a tree, so the malformed value is
3391
+ * recovered leniently *and* reported. The lenient reading survives into the
3392
+ * tree: `lower` is handed a `Program` and never sees a diagnostic, so a
3393
+ * caller must reject on `severity === 'error'` before lowering, or `"a\xb"`
3394
+ * reaches the engine as `axb`.
3395
+ */
3396
+ private unescapeString(raw: string, tok?: Token): string {
3397
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
3398
+ try {
3399
+ return JSON.parse(raw) as string
3400
+ } catch {
3401
+ if (tok) {
3402
+ this.error(
3403
+ `Invalid string literal ${raw}: KIP strings are JSON strings`,
3404
+ tok
3405
+ )
3406
+ }
3407
+ raw = raw.slice(1, -1)
3408
+ }
3409
+ } else if (tok) {
3410
+ this.error(`Unterminated string literal ${raw}`, tok)
3411
+ }
3412
+ return raw.replace(/\\(.)/g, (_, ch) => {
3413
+ switch (ch) {
3414
+ case 'n':
3415
+ return '\n'
3416
+ case 't':
3417
+ return '\t'
3418
+ case 'r':
3419
+ return '\r'
3420
+ case '\\':
3421
+ return '\\'
3422
+ case '"':
3423
+ return '"'
3424
+ case '/':
3425
+ return '/'
3426
+ default:
3427
+ return ch
3428
+ }
3429
+ })
3430
+ }
3431
+
3432
+ private error(message: string, token: Token, code = 'KIP_1001'): void {
3433
+ this.diagnostics.push({
3434
+ range: {
3435
+ start: { line: token.line, column: token.column },
3436
+ end: { line: token.line, column: token.column + token.value.length }
3437
+ },
3438
+ severity: 'error',
3439
+ message,
3440
+ code
3441
+ })
3442
+ }
3443
+
3444
+ private static readonly STATEMENT_STARTERS: ReadonlySet<TokenType> = new Set([
3445
+ TokenType.Find,
3446
+ TokenType.Mutate,
3447
+ TokenType.Create,
3448
+ TokenType.Upsert,
3449
+ TokenType.Ensure,
3450
+ TokenType.Assert,
3451
+ TokenType.Update,
3452
+ TokenType.Retract,
3453
+ TokenType.Supersede,
3454
+ TokenType.Correct,
3455
+ TokenType.Transition,
3456
+ TokenType.Set,
3457
+ TokenType.Archive,
3458
+ TokenType.Tombstone,
3459
+ TokenType.Purge,
3460
+ TokenType.Merge,
3461
+ TokenType.Describe,
3462
+ TokenType.List,
3463
+ TokenType.Search,
3464
+ TokenType.Verify,
3465
+ TokenType.Validate,
3466
+ TokenType.Preview,
3467
+ TokenType.History,
3468
+ TokenType.Changes,
3469
+ TokenType.Snapshot,
3470
+ TokenType.Export,
3471
+ TokenType.EOF
3472
+ ])
3473
+
3474
+ private recoverToNextStatement(): void {
3475
+ while (
3476
+ !this.isAtEnd() &&
3477
+ !Parser.STATEMENT_STARTERS.has(this.current().type)
3478
+ ) {
3479
+ this.pos++
3480
+ }
3481
+ }
3482
+
3483
+ /** Inside MUTATE, recovery stops at the next clause or the closing brace. */
3484
+ private recoverToMutationBoundary(): void {
3485
+ let depth = 0
3486
+ while (!this.isAtEnd()) {
3487
+ const type = this.current().type
3488
+ if (type === TokenType.LBrace) depth++
3489
+ else if (type === TokenType.RBrace) {
3490
+ if (depth === 0) return
3491
+ depth--
3492
+ } else if (depth === 0 && Parser.STATEMENT_STARTERS.has(type)) {
3493
+ return
3494
+ }
3495
+ this.pos++
3496
+ }
3497
+ }
3498
+ }
3499
+
3500
+ /** Unwinds a sub-parser that cannot produce a node; `parse` recovers. */
3501
+ class ParseAbort extends Error {
3502
+ constructor() {
3503
+ super('parse aborted')
3504
+ this.name = 'ParseAbort'
3505
+ }
3506
+ }