@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/dist/parser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { tokenize } from './lexer.js';
2
- import { TokenType, isTrivia } from './token.js';
2
+ import { TokenType, isTrivia, isIdentifierLike, isAggregate } from './token.js';
3
3
  export function parse(source) {
4
4
  const allTokens = tokenize(source);
5
5
  const parser = new Parser(allTokens, source);
@@ -10,6 +10,7 @@ class Parser {
10
10
  pos = 0;
11
11
  diagnostics = [];
12
12
  source;
13
+ dialect = 'kql';
13
14
  constructor(tokens, source) {
14
15
  // Filter out trivia for parsing, but keep comments for attachment later
15
16
  this.tokens = tokens.filter((t) => !isTrivia(t.type) || t.type === TokenType.Comment);
@@ -51,1223 +52,2104 @@ class Parser {
51
52
  parseStatement() {
52
53
  const tok = this.current();
53
54
  switch (tok.type) {
55
+ // KQL
54
56
  case TokenType.Find:
55
57
  return this.parseFindStatement();
58
+ // KML
59
+ case TokenType.Mutate:
60
+ return this.parseMutateStatement();
61
+ case TokenType.Create:
56
62
  case TokenType.Upsert:
57
- return this.parseUpsertStatement();
63
+ case TokenType.Ensure:
64
+ case TokenType.Assert:
58
65
  case TokenType.Update:
59
- return this.parseUpdateStatement();
66
+ case TokenType.Retract:
67
+ case TokenType.Supersede:
68
+ case TokenType.Correct:
69
+ case TokenType.Transition:
70
+ case TokenType.Set:
71
+ case TokenType.Archive:
72
+ case TokenType.Tombstone:
73
+ case TokenType.Purge:
60
74
  case TokenType.Merge:
61
- return this.parseMergeStatement();
62
- case TokenType.Delete:
63
- return this.parseDeleteStatement();
75
+ return this.parseMutationClause();
76
+ // META
64
77
  case TokenType.Describe:
65
78
  return this.parseDescribeStatement();
79
+ case TokenType.List:
80
+ return this.parseListStatement();
66
81
  case TokenType.Search:
67
82
  return this.parseSearchStatement();
83
+ case TokenType.Verify:
84
+ return this.parseVerifyStatement();
85
+ case TokenType.Validate:
86
+ return this.parseValidateStatement();
87
+ case TokenType.Preview:
88
+ return this.parsePreviewStatement();
89
+ case TokenType.History:
90
+ return this.parseHistoryStatement();
91
+ case TokenType.Changes:
92
+ return this.parseChangesStatement();
93
+ case TokenType.Snapshot:
94
+ return this.parseSnapshotStatement();
68
95
  case TokenType.Export:
69
- return this.parseExportStatement();
96
+ return this.parseExportCapsuleStatement();
70
97
  default:
71
- this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, UPDATE, MERGE, DELETE, DESCRIBE, SEARCH, EXPORT)`, tok);
72
- this.advance();
98
+ this.error(`Unexpected token '${tok.value}': expected a KQL, KML or META statement`, tok);
73
99
  return null;
74
100
  }
75
101
  }
102
+ /** Every mutation legal at statement level and inside `MUTATE { ... }`. */
103
+ parseMutationClause() {
104
+ const tok = this.current();
105
+ switch (tok.type) {
106
+ case TokenType.Create:
107
+ return this.parseCreateStatement();
108
+ case TokenType.Upsert:
109
+ return this.parseUpsertConcept();
110
+ case TokenType.Ensure:
111
+ return this.parseEnsureProposition();
112
+ case TokenType.Assert:
113
+ return this.parseAssertStatement();
114
+ case TokenType.Update:
115
+ return this.parseUpdateStatement();
116
+ case TokenType.Retract:
117
+ return this.parseRetractAssertion();
118
+ case TokenType.Supersede:
119
+ return this.parseSupersedeAssertion();
120
+ case TokenType.Correct:
121
+ return this.parseCorrectEvidence();
122
+ case TokenType.Transition:
123
+ return this.parseTransitionActivity();
124
+ case TokenType.Set:
125
+ return this.parseSetRetention();
126
+ case TokenType.Archive:
127
+ return this.parseArchiveStatement();
128
+ case TokenType.Tombstone:
129
+ return this.parseTombstoneStatement();
130
+ case TokenType.Purge:
131
+ return this.parsePurgeStatement();
132
+ case TokenType.Merge:
133
+ return this.parseMergeConcept();
134
+ default:
135
+ this.error(`Unexpected token '${tok.value}': expected a KML mutation`, tok);
136
+ throw new ParseAbort();
137
+ }
138
+ }
76
139
  // ────────────────────────────────────────────────────────────────────
77
- // FIND
140
+ // KQL — FIND
78
141
  // ────────────────────────────────────────────────────────────────────
79
142
  parseFindStatement() {
143
+ const leadingComments = this.collectLeadingComments();
80
144
  const start = this.currentPos();
81
- const comments = this.collectLeadingComments();
145
+ this.dialect = 'kql';
82
146
  this.expect(TokenType.Find);
83
- const lparen = this.current();
84
147
  this.expect(TokenType.LParen);
85
148
  const projections = [];
86
149
  if (!this.check(TokenType.RParen)) {
87
- projections.push(this.parseExpression());
88
- while (this.match(TokenType.Comma)) {
89
- projections.push(this.parseExpression());
90
- }
150
+ do {
151
+ projections.push(this.parseProjectionExpression());
152
+ } while (this.match(TokenType.Comma));
91
153
  }
92
154
  if (projections.length === 0) {
93
- this.error(`FIND must declare at least one output expression, e.g. FIND(?var)`, lparen);
155
+ this.error('FIND requires at least one projection', this.current());
94
156
  }
95
157
  this.expect(TokenType.RParen);
96
- let where;
158
+ this.expectKeywordWithSpace(TokenType.Where);
159
+ const where = this.parseWhereClause();
160
+ let asOf;
161
+ let forTime;
162
+ let epistemic;
97
163
  let orderBy;
98
164
  let limit;
99
165
  let cursor;
100
- if (this.check(TokenType.Where)) {
101
- where = this.parseWhereClause();
102
- }
103
- if (this.check(TokenType.Order)) {
104
- orderBy = this.parseOrderBy();
105
- }
106
- if (this.check(TokenType.Limit)) {
107
- limit = this.parseLimitClause();
108
- }
109
- if (this.check(TokenType.Cursor)) {
110
- cursor = this.parseCursorClause();
166
+ let lastClauseOrder = -1;
167
+ // The grammar fixes this order. Accepting any order here would let a
168
+ // command run on this parser that a conformant engine rejects, so each
169
+ // clause is taken once and out-of-order repeats are reported.
170
+ for (;;) {
171
+ const tok = this.current();
172
+ if (this.check(TokenType.As)) {
173
+ lastClauseOrder = this.checkClauseOrder(0, lastClauseOrder, 'AS OF', tok);
174
+ this.rejectRepeat(asOf, 'AS OF', tok);
175
+ asOf = this.parseAsOfClause();
176
+ }
177
+ else if (this.check(TokenType.For)) {
178
+ lastClauseOrder = this.checkClauseOrder(1, lastClauseOrder, 'FOR TIME', tok);
179
+ this.rejectRepeat(forTime, 'FOR TIME', tok);
180
+ forTime = this.parseForTimeClause();
181
+ }
182
+ else if (this.check(TokenType.With)) {
183
+ lastClauseOrder = this.checkClauseOrder(2, lastClauseOrder, 'WITH EPISTEMIC', tok);
184
+ this.rejectRepeat(epistemic, 'WITH EPISTEMIC', tok);
185
+ epistemic = this.parseEpistemicClause();
186
+ }
187
+ else if (this.check(TokenType.Order)) {
188
+ lastClauseOrder = this.checkClauseOrder(3, lastClauseOrder, 'ORDER BY', tok);
189
+ this.rejectRepeat(orderBy, 'ORDER BY', tok);
190
+ orderBy = this.parseOrderBy();
191
+ }
192
+ else if (this.check(TokenType.Limit)) {
193
+ lastClauseOrder = this.checkClauseOrder(4, lastClauseOrder, 'LIMIT', tok);
194
+ this.rejectRepeat(limit, 'LIMIT', tok);
195
+ limit = this.parseLimitClause();
196
+ }
197
+ else if (this.check(TokenType.Cursor)) {
198
+ lastClauseOrder = this.checkClauseOrder(5, lastClauseOrder, 'CURSOR', tok);
199
+ this.rejectRepeat(cursor, 'CURSOR', tok);
200
+ cursor = this.parseCursorClause();
201
+ }
202
+ else {
203
+ break;
204
+ }
111
205
  }
112
206
  return {
113
207
  kind: 'FindStatement',
114
208
  projections,
115
209
  where,
210
+ asOf,
211
+ forTime,
212
+ epistemic,
116
213
  orderBy,
117
214
  limit,
118
215
  cursor,
119
- range: { start, end: this.currentPos() },
120
- leadingComments: comments.length > 0 ? comments : undefined
216
+ range: { start, end: this.endPos() },
217
+ leadingComments: leadingComments.length ? leadingComments : undefined
121
218
  };
122
219
  }
123
- // ────────────────────────────────────────────────────────────────────
124
- // UPSERT
125
- // ────────────────────────────────────────────────────────────────────
126
- parseUpsertStatement() {
220
+ /** `projection_expression = aggregate_expression | expression` */
221
+ parseProjectionExpression() {
222
+ return this.parseExpression();
223
+ }
224
+ parseAsOfClause() {
127
225
  const start = this.currentPos();
128
- const comments = this.collectLeadingComments();
129
- this.expectKeywordWithSpace(TokenType.Upsert);
130
- this.expect(TokenType.LBrace);
131
- const blocks = [];
132
- this.skipComments();
133
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
134
- const before = this.pos;
135
- this.skipComments();
136
- if (this.check(TokenType.Concept)) {
137
- blocks.push(this.parseConceptBlock());
138
- }
139
- else if (this.check(TokenType.Proposition)) {
140
- blocks.push(this.parsePropositionBlock());
141
- }
142
- else if (this.check(TokenType.RBrace)) {
143
- break;
144
- }
145
- else {
146
- this.error(`Expected CONCEPT or PROPOSITION inside UPSERT block`, this.current());
147
- this.advance();
148
- }
149
- this.skipComments();
150
- // A sub-parser that rejects its first token reports and returns
151
- // without consuming it, so a loop keyed on that token would spin
152
- // forever building diagnostics. Stop as soon as nothing moved.
153
- if (this.pos === before)
154
- break;
226
+ const first = this.expect(TokenType.As);
227
+ this.expectSecondWord(TokenType.Of, first);
228
+ let basis;
229
+ if (this.match(TokenType.Seq)) {
230
+ basis = 'SEQ';
155
231
  }
156
- this.expect(TokenType.RBrace);
157
- let metadata;
158
- if (this.check(TokenType.With)) {
159
- metadata = this.parseWithMetadata();
232
+ else if (this.match(TokenType.Tx)) {
233
+ basis = 'TX';
234
+ }
235
+ else if (this.match(TokenType.Time)) {
236
+ basis = 'TIME';
160
237
  }
238
+ else {
239
+ this.error(`Expected SEQ, TX or TIME after AS OF but got '${this.current().value}'`, this.current());
240
+ basis = 'SEQ';
241
+ }
242
+ const value = this.parseScalarValue();
161
243
  return {
162
- kind: 'UpsertStatement',
163
- blocks,
164
- metadata,
165
- range: { start, end: this.currentPos() },
166
- leadingComments: comments.length > 0 ? comments : undefined
244
+ kind: 'AsOfClause',
245
+ basis,
246
+ value,
247
+ range: { start, end: this.endPos() }
167
248
  };
168
249
  }
169
- parseConceptBlock() {
250
+ parseForTimeClause() {
170
251
  const start = this.currentPos();
171
- const comments = this.collectLeadingComments();
172
- this.expectKeywordWithSpace(TokenType.Concept);
173
- // The handle is optional: a block nothing else refers to needs no name.
174
- let handle;
175
- if (this.check(TokenType.Variable)) {
176
- handle = this.expectVariable();
177
- }
178
- this.expect(TokenType.LBrace);
179
- const matcher = this.parseConceptMatcher();
180
- let expectVersion;
181
- if (this.check(TokenType.Expect)) {
182
- expectVersion = this.parseExpectVersion();
183
- }
184
- let setAttributes;
185
- let setPropositions;
186
- let metadata;
187
- this.skipComments();
188
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
189
- const before = this.pos;
190
- this.skipComments();
191
- if (this.check(TokenType.Set)) {
192
- const setStart = this.currentPos();
193
- const setTok = this.advance(); // skip SET
194
- if (this.check(TokenType.Attributes)) {
195
- this.expectSecondWord(TokenType.Attributes, setTok);
196
- setAttributes = this.parseSetAttributesBody(setStart);
197
- }
198
- else if (this.check(TokenType.Propositions)) {
199
- this.expectSecondWord(TokenType.Propositions, setTok);
200
- setPropositions = this.parseSetPropositionsBody(setStart);
201
- }
202
- else {
203
- this.error(`Expected ATTRIBUTES or PROPOSITIONS after SET`, this.current());
204
- this.advance();
205
- }
206
- }
207
- else if (this.check(TokenType.With)) {
208
- metadata = this.parseWithMetadata();
209
- }
210
- else if (this.check(TokenType.RBrace)) {
211
- break;
212
- }
213
- else {
214
- this.skipComments();
215
- if (this.check(TokenType.RBrace))
216
- break;
217
- this.error(`Unexpected token '${this.current().value}' in CONCEPT block`, this.current());
218
- this.advance();
219
- }
220
- this.skipComments();
221
- // A sub-parser that rejects its first token reports and returns
222
- // without consuming it, so a loop keyed on that token would spin
223
- // forever building diagnostics. Stop as soon as nothing moved.
224
- if (this.pos === before)
225
- break;
226
- }
227
- this.expect(TokenType.RBrace);
228
- // Concept-level WITH METADATA (outside the CONCEPT braces)
229
- if (!metadata && this.check(TokenType.With)) {
230
- metadata = this.parseWithMetadata();
231
- }
252
+ const first = this.expect(TokenType.For);
253
+ this.expectSecondWord(TokenType.Time, first);
254
+ const value = this.parseScalarValue();
232
255
  return {
233
- kind: 'ConceptBlock',
234
- handle,
235
- matcher,
236
- expectVersion,
237
- setAttributes,
238
- setPropositions,
239
- metadata,
240
- range: { start, end: this.currentPos() },
241
- leadingComments: comments.length > 0 ? comments : undefined
256
+ kind: 'ForTimeClause',
257
+ value,
258
+ range: { start, end: this.endPos() }
242
259
  };
243
260
  }
244
- parsePropositionBlock() {
261
+ parseEpistemicClause() {
245
262
  const start = this.currentPos();
246
- const comments = this.collectLeadingComments();
247
- this.expectKeywordWithSpace(TokenType.Proposition);
248
- let handle;
249
- if (this.check(TokenType.Variable)) {
250
- handle = this.expectVariable();
251
- }
252
- this.expect(TokenType.LBrace);
253
- const proposition = this.parsePropositionPatternBody(undefined);
254
- let expectVersion;
255
- if (this.check(TokenType.Expect)) {
256
- expectVersion = this.parseExpectVersion();
257
- }
258
- let setAttributes;
259
- let metadata;
260
- this.skipComments();
261
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
262
- const before = this.pos;
263
- this.skipComments();
264
- if (this.check(TokenType.Set)) {
265
- const setStart = this.currentPos();
266
- const setTok = this.advance();
267
- if (this.check(TokenType.Attributes)) {
268
- this.expectSecondWord(TokenType.Attributes, setTok);
269
- setAttributes = this.parseSetAttributesBody(setStart);
270
- }
271
- else {
272
- this.error(`Expected ATTRIBUTES after SET in PROPOSITION block`, this.current());
273
- this.advance();
274
- }
275
- }
276
- else if (this.check(TokenType.RBrace)) {
277
- break;
278
- }
279
- else {
280
- this.error(`Unexpected token '${this.current().value}' in PROPOSITION block`, this.current());
281
- this.advance();
282
- }
283
- this.skipComments();
284
- // A sub-parser that rejects its first token reports and returns
285
- // without consuming it, so a loop keyed on that token would spin
286
- // forever building diagnostics. Stop as soon as nothing moved.
287
- if (this.pos === before)
288
- break;
289
- }
290
- this.expect(TokenType.RBrace);
291
- if (this.check(TokenType.With)) {
292
- metadata = this.parseWithMetadata();
293
- }
263
+ const first = this.expect(TokenType.With);
264
+ this.expectSecondWord(TokenType.Epistemic, first);
265
+ const options = this.parseObjectLiteral();
294
266
  return {
295
- kind: 'PropositionBlock',
296
- handle,
297
- id: proposition.id,
298
- subject: proposition.subject,
299
- predicate: proposition.predicate,
300
- object: proposition.object,
301
- expectVersion,
302
- setAttributes,
303
- metadata,
304
- range: { start, end: this.currentPos() },
305
- leadingComments: comments.length > 0 ? comments : undefined
267
+ kind: 'EpistemicClause',
268
+ options,
269
+ range: { start, end: this.endPos() }
306
270
  };
307
271
  }
308
- // ────────────────────────────────────────────────────────────────────
309
- // UPDATE
310
- // ────────────────────────────────────────────────────────────────────
311
- parseUpdateStatement() {
272
+ parseOrderBy() {
312
273
  const start = this.currentPos();
313
- const comments = this.collectLeadingComments();
314
- this.expectKeywordWithSpace(TokenType.Update);
315
- const target = this.expectVariable();
316
- let setAttributes;
317
- let setMetadata;
318
- this.skipComments();
319
- while (this.check(TokenType.Set) && !this.isAtEnd()) {
320
- const before = this.pos;
321
- const setStart = this.currentPos();
322
- const setTok = this.advance();
323
- if (this.check(TokenType.Attributes)) {
324
- this.expectSecondWord(TokenType.Attributes, setTok);
325
- setAttributes = this.parseSetAttributesBody(setStart);
326
- }
327
- else if (this.check(TokenType.Metadata)) {
328
- this.expectSecondWord(TokenType.Metadata, setTok);
329
- setMetadata = this.parseSetMetadataBody(setStart);
330
- }
331
- else {
332
- this.error(`Expected ATTRIBUTES or METADATA after SET in UPDATE statement`, this.current());
333
- this.advance();
334
- }
335
- this.skipComments();
336
- // A sub-parser that rejects its first token reports and returns
337
- // without consuming it, so a loop keyed on that token would spin
338
- // forever building diagnostics. Stop as soon as nothing moved.
339
- if (this.pos === before)
340
- break;
341
- }
342
- if (!setAttributes && !setMetadata) {
343
- this.error(`Expected SET ATTRIBUTES or SET METADATA in UPDATE statement`, this.current());
344
- }
345
- const where = this.parseWhereClause();
346
- let limit;
347
- if (this.check(TokenType.Limit)) {
348
- limit = this.parseLimitClause();
349
- }
274
+ const first = this.expect(TokenType.Order);
275
+ this.expectSecondWord(TokenType.By, first);
276
+ const items = [];
277
+ do {
278
+ items.push(this.parseOrderItem());
279
+ } while (this.match(TokenType.Comma));
350
280
  return {
351
- kind: 'UpdateStatement',
352
- target,
353
- setAttributes,
354
- setMetadata,
355
- where,
356
- limit,
357
- range: { start, end: this.currentPos() },
358
- leadingComments: comments.length > 0 ? comments : undefined
281
+ kind: 'OrderByClause',
282
+ items,
283
+ range: { start, end: this.endPos() }
359
284
  };
360
285
  }
361
- // ────────────────────────────────────────────────────────────────────
362
- // MERGE
363
- // ────────────────────────────────────────────────────────────────────
364
- parseMergeStatement() {
286
+ parseOrderItem() {
365
287
  const start = this.currentPos();
366
- const comments = this.collectLeadingComments();
367
- const mergeTok = this.expect(TokenType.Merge);
368
- this.expectSecondWord(TokenType.Concept, mergeTok);
369
- const source = this.expectVariable();
370
- this.expect(TokenType.Into);
371
- const target = this.expectVariable();
372
- const where = this.parseWhereClause();
288
+ const expression = this.parseProjectionExpression();
289
+ let direction;
290
+ if (this.match(TokenType.Asc))
291
+ direction = 'ASC';
292
+ else if (this.match(TokenType.Desc))
293
+ direction = 'DESC';
373
294
  return {
374
- kind: 'MergeStatement',
375
- source,
376
- target,
377
- where,
378
- range: { start, end: this.currentPos() },
379
- leadingComments: comments.length > 0 ? comments : undefined
295
+ kind: 'OrderItem',
296
+ expression,
297
+ direction,
298
+ range: { start, end: this.endPos() }
380
299
  };
381
300
  }
382
- // ────────────────────────────────────────────────────────────────────
383
- // DELETE
384
- // ────────────────────────────────────────────────────────────────────
385
- parseDeleteStatement() {
301
+ parseLimitClause() {
386
302
  const start = this.currentPos();
387
- const comments = this.collectLeadingComments();
388
- this.expectKeywordWithSpace(TokenType.Delete);
389
- let deleteType;
390
- let keys;
391
- let target;
392
- let detach = false;
393
- if (this.check(TokenType.Attributes)) {
394
- deleteType = 'ATTRIBUTES';
395
- this.advance();
396
- keys = this.parseDeleteKeySet();
397
- this.expect(TokenType.From);
398
- target = this.expectVariable();
399
- }
400
- else if (this.check(TokenType.Metadata)) {
401
- deleteType = 'METADATA';
402
- this.advance();
403
- keys = this.parseDeleteKeySet();
404
- this.expect(TokenType.From);
405
- target = this.expectVariable();
406
- }
407
- else if (this.check(TokenType.Propositions)) {
408
- deleteType = 'PROPOSITIONS';
409
- this.advance();
410
- target = this.expectVariable();
411
- }
412
- else if (this.check(TokenType.Concept)) {
413
- deleteType = 'CONCEPT';
414
- this.advance();
415
- target = this.expectVariable();
416
- if (this.check(TokenType.Detach)) {
417
- detach = true;
418
- this.advance();
419
- }
420
- else {
421
- this.error(`Expected DETACH after DELETE CONCEPT target '${target}'`, this.current());
422
- }
423
- }
424
- else {
425
- this.error(`Expected ATTRIBUTES, METADATA, PROPOSITIONS, or CONCEPT after DELETE`, this.current());
426
- deleteType = 'ATTRIBUTES';
427
- target = '?unknown';
428
- }
429
- const where = this.parseWhereClause();
303
+ this.expect(TokenType.Limit);
304
+ const value = this.parseScalarValue();
430
305
  return {
431
- kind: 'DeleteStatement',
432
- deleteType,
433
- keys,
434
- target,
435
- detach: detach || undefined,
436
- where,
437
- range: { start, end: this.currentPos() },
438
- leadingComments: comments.length > 0 ? comments : undefined
306
+ kind: 'LimitClause',
307
+ value,
308
+ range: { start, end: this.endPos() }
439
309
  };
440
310
  }
441
- parseDeleteKeySet() {
442
- this.expect(TokenType.LBrace);
443
- const keys = [];
444
- if (!this.check(TokenType.RBrace)) {
445
- keys.push(this.expectString());
446
- while (this.match(TokenType.Comma)) {
447
- if (this.check(TokenType.RBrace))
448
- break;
449
- keys.push(this.expectString());
450
- }
451
- }
452
- this.expect(TokenType.RBrace);
453
- return keys;
454
- }
455
- // ────────────────────────────────────────────────────────────────────
456
- // DESCRIBE
457
- // ────────────────────────────────────────────────────────────────────
458
- parseDescribeStatement() {
311
+ parseCursorClause() {
459
312
  const start = this.currentPos();
460
- const comments = this.collectLeadingComments();
461
- this.expectKeywordWithSpace(TokenType.Describe);
462
- let describeType;
463
- let typeName;
464
- let typeNameValue;
465
- let limit;
466
- let cursor;
467
- if (this.check(TokenType.Primer)) {
468
- describeType = 'PRIMER';
469
- this.advance();
470
- }
471
- else if (this.check(TokenType.Domains)) {
472
- describeType = 'DOMAINS';
473
- this.advance();
474
- }
475
- else if (this.check(TokenType.Concept)) {
476
- const headTok = this.advance();
477
- if (this.check(TokenType.Types)) {
478
- describeType = 'CONCEPT_TYPES';
479
- this.expectSecondWord(TokenType.Types, headTok);
480
- }
481
- else if (this.check(TokenType.Type)) {
482
- describeType = 'CONCEPT_TYPE';
483
- this.expectSecondWord(TokenType.Type, headTok);
484
- typeNameValue = this.parseStringOrParameterValue('DESCRIBE CONCEPT TYPE');
485
- typeName =
486
- typeNameValue.kind === 'StringLiteral'
487
- ? typeNameValue.parsed
488
- : typeNameValue.name;
489
- }
490
- else {
491
- this.error(`Expected TYPE or TYPES after DESCRIBE CONCEPT`, this.current());
492
- describeType = 'CONCEPT_TYPES';
493
- }
494
- }
495
- else if (this.check(TokenType.Proposition)) {
496
- const headTok = this.advance();
497
- if (this.check(TokenType.Types)) {
498
- describeType = 'PROPOSITION_TYPES';
499
- this.expectSecondWord(TokenType.Types, headTok);
500
- }
501
- else if (this.check(TokenType.Type)) {
502
- describeType = 'PROPOSITION_TYPE';
503
- this.expectSecondWord(TokenType.Type, headTok);
504
- typeNameValue = this.parseStringOrParameterValue('DESCRIBE PROPOSITION TYPE');
505
- typeName =
506
- typeNameValue.kind === 'StringLiteral'
507
- ? typeNameValue.parsed
508
- : typeNameValue.name;
509
- }
510
- else {
511
- this.error(`Expected TYPE or TYPES after DESCRIBE PROPOSITION`, this.current());
512
- describeType = 'PROPOSITION_TYPES';
513
- }
514
- }
515
- else {
516
- this.error(`Expected PRIMER, DOMAINS, CONCEPT, or PROPOSITION after DESCRIBE`, this.current());
517
- describeType = 'PRIMER';
518
- }
519
- // Only the plural `... TYPES` forms are paginated (§5.1.3 / §5.1.5).
520
- const paginable = describeType === 'CONCEPT_TYPES' || describeType === 'PROPOSITION_TYPES';
521
- if (this.check(TokenType.Limit)) {
522
- if (!paginable) {
523
- this.error(`LIMIT is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
524
- }
525
- limit = this.parseLimitClause();
526
- }
527
- if (this.check(TokenType.Cursor)) {
528
- if (!paginable) {
529
- this.error(`CURSOR is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
530
- }
531
- cursor = this.parseCursorClause();
532
- }
313
+ this.expect(TokenType.Cursor);
314
+ const value = this.parseScalarValue();
533
315
  return {
534
- kind: 'DescribeStatement',
535
- describeType,
536
- typeName,
537
- typeNameValue,
538
- limit,
539
- cursor,
540
- range: { start, end: this.currentPos() },
541
- leadingComments: comments.length > 0 ? comments : undefined
316
+ kind: 'CursorClause',
317
+ value,
318
+ range: { start, end: this.endPos() }
542
319
  };
543
320
  }
544
321
  // ────────────────────────────────────────────────────────────────────
545
- // SEARCH
322
+ // WHERE
546
323
  // ────────────────────────────────────────────────────────────────────
547
- parseSearchStatement() {
324
+ parseWhereClause() {
548
325
  const start = this.currentPos();
549
- const comments = this.collectLeadingComments();
550
- this.expectKeywordWithSpace(TokenType.Search);
551
- let searchTarget;
552
- if (this.check(TokenType.Concept)) {
553
- searchTarget = 'CONCEPT';
554
- this.expectKeywordWithSpace(TokenType.Concept);
555
- }
556
- else if (this.check(TokenType.Proposition)) {
557
- searchTarget = 'PROPOSITION';
558
- this.expectKeywordWithSpace(TokenType.Proposition);
559
- }
560
- else {
561
- this.error(`Expected CONCEPT or PROPOSITION after SEARCH`, this.current());
562
- searchTarget = 'CONCEPT';
563
- }
564
- const termValue = this.parseStringOrParameterValue('SEARCH term');
565
- const term = termValue.kind === 'StringLiteral' ? termValue.parsed : termValue.name;
566
- let withType;
567
- let withTypeValue;
568
- let mode;
569
- let modeValue;
570
- let threshold;
571
- let limit;
572
- // Clauses may appear in any order but each at most once — a second
573
- // `LIMIT` is trailing input, not an override.
574
- while (!this.isAtEnd()) {
326
+ this.expect(TokenType.LBrace);
327
+ const patterns = this.parseWherePatterns();
328
+ this.expect(TokenType.RBrace);
329
+ return {
330
+ kind: 'WhereClause',
331
+ patterns,
332
+ range: { start, end: this.endPos() }
333
+ };
334
+ }
335
+ parseWherePatterns() {
336
+ const patterns = [];
337
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
575
338
  const before = this.pos;
576
- const clause = this.current();
577
- if (this.check(TokenType.With)) {
578
- this.rejectRepeat(withTypeValue, 'WITH TYPE', clause);
579
- const withTok = this.advance();
580
- this.expectSecondWord(TokenType.Type, withTok);
581
- withTypeValue = this.parseStringOrParameterValue('SEARCH WITH TYPE');
582
- withType =
583
- withTypeValue.kind === 'StringLiteral'
584
- ? withTypeValue.parsed
585
- : withTypeValue.name;
339
+ const pattern = this.parseWherePattern();
340
+ if (pattern)
341
+ patterns.push(pattern);
342
+ if (this.pos === before)
343
+ break;
344
+ // WHERE items are whitespace-delimited; a comma between them is not
345
+ // grammar, so report it rather than silently accepting both spellings.
346
+ if (this.check(TokenType.Comma)) {
347
+ this.error('WHERE items are separated by whitespace, not commas', this.current());
348
+ this.advance();
349
+ }
350
+ }
351
+ return patterns;
352
+ }
353
+ parseWherePattern() {
354
+ const tok = this.current();
355
+ switch (tok.type) {
356
+ case TokenType.Variable:
357
+ return this.parseVariableLeadingPattern();
358
+ case TokenType.LParen:
359
+ case TokenType.Proposition:
360
+ return this.parsePropositionPattern(undefined);
361
+ case TokenType.Structural:
362
+ return this.parseStructuralPattern(undefined);
363
+ case TokenType.Filter:
364
+ return this.parseFilterClause();
365
+ case TokenType.Not:
366
+ return this.parseNotClause();
367
+ case TokenType.Optional:
368
+ return this.parseOptionalClause();
369
+ case TokenType.Union:
370
+ return this.parseUnionClause();
371
+ default:
372
+ this.error(`Unexpected token '${tok.value}' in WHERE block`, tok);
373
+ this.advance();
374
+ return null;
375
+ }
376
+ }
377
+ /**
378
+ * Disambiguates the pattern families that all begin with a variable.
379
+ *
380
+ * `?x {`/`?x CONCEPT {` is a Concept, `?x (`/`?x PROPOSITION (` a raw
381
+ * Proposition, and the remaining families name their kind outright.
382
+ */
383
+ parseVariableLeadingPattern() {
384
+ const variable = this.parseVariableRef();
385
+ const tok = this.current();
386
+ switch (tok.type) {
387
+ case TokenType.LBrace:
388
+ case TokenType.Concept:
389
+ return this.parseConceptPattern(variable);
390
+ case TokenType.LParen:
391
+ case TokenType.Proposition:
392
+ return this.parsePropositionPattern(variable);
393
+ case TokenType.Assertion:
394
+ return this.parseAssertionPattern(variable);
395
+ case TokenType.Evidence:
396
+ return this.parseEvidencePattern(variable);
397
+ case TokenType.Activity:
398
+ return this.parseActivityPattern(variable);
399
+ case TokenType.Structural:
400
+ return this.parseStructuralPattern(variable);
401
+ case TokenType.Belief:
402
+ return this.parseBeliefPattern(variable);
403
+ default:
404
+ this.error(`Expected a pattern body after ${variable.name} but got '${tok.value}'`, tok);
405
+ throw new ParseAbort();
406
+ }
407
+ }
408
+ parseConceptPattern(variable) {
409
+ const explicit = this.match(TokenType.Concept);
410
+ const matcher = this.parseObjectPattern();
411
+ return {
412
+ kind: 'ConceptPattern',
413
+ variable,
414
+ explicit,
415
+ matcher,
416
+ range: { start: variable.range.start, end: this.endPos() }
417
+ };
418
+ }
419
+ parsePropositionPattern(variable) {
420
+ const start = variable ? variable.range.start : this.currentPos();
421
+ const explicit = this.match(TokenType.Proposition);
422
+ const tuple = this.parsePropositionTuple();
423
+ return {
424
+ kind: 'PropositionPattern',
425
+ variable,
426
+ explicit,
427
+ tuple,
428
+ range: { start, end: this.endPos() }
429
+ };
430
+ }
431
+ parseAssertionPattern(variable) {
432
+ this.expect(TokenType.Assertion);
433
+ const matcher = this.parseObjectPattern();
434
+ return {
435
+ kind: 'AssertionPattern',
436
+ variable,
437
+ matcher,
438
+ range: { start: variable.range.start, end: this.endPos() }
439
+ };
440
+ }
441
+ parseEvidencePattern(variable) {
442
+ this.expect(TokenType.Evidence);
443
+ const matcher = this.parseObjectPattern();
444
+ return {
445
+ kind: 'EvidencePattern',
446
+ variable,
447
+ matcher,
448
+ range: { start: variable.range.start, end: this.endPos() }
449
+ };
450
+ }
451
+ parseActivityPattern(variable) {
452
+ this.expect(TokenType.Activity);
453
+ const matcher = this.parseObjectPattern();
454
+ return {
455
+ kind: 'ActivityPattern',
456
+ variable,
457
+ matcher,
458
+ range: { start: variable.range.start, end: this.endPos() }
459
+ };
460
+ }
461
+ parseStructuralPattern(variable) {
462
+ const start = variable ? variable.range.start : this.currentPos();
463
+ this.expect(TokenType.Structural);
464
+ this.expect(TokenType.LParen);
465
+ const subject = this.parseTerm();
466
+ this.expect(TokenType.Comma);
467
+ const field = this.parseSchemaSymbol();
468
+ this.expect(TokenType.Comma);
469
+ const object = this.parseTerm();
470
+ this.expect(TokenType.RParen);
471
+ return {
472
+ kind: 'StructuralPattern',
473
+ variable,
474
+ subject,
475
+ field,
476
+ object,
477
+ range: { start, end: this.endPos() }
478
+ };
479
+ }
480
+ parseBeliefPattern(variable) {
481
+ const start = variable.range.start;
482
+ const beliefTok = this.expect(TokenType.Belief);
483
+ // `BELIEF SLOT (...)` — the whole functional slot, not one tuple.
484
+ if (this.check(TokenType.Slot)) {
485
+ this.expectSecondWord(TokenType.Slot, beliefTok);
486
+ this.expect(TokenType.LParen);
487
+ const subject = this.parseTerm();
488
+ this.expect(TokenType.Comma);
489
+ const predicate = this.parsePredicateAtom();
490
+ this.expect(TokenType.RParen);
491
+ this.rejectBeliefInRawDialect(start);
492
+ return {
493
+ kind: 'BeliefSlotPattern',
494
+ variable,
495
+ subject,
496
+ predicate,
497
+ range: { start, end: this.endPos() }
498
+ };
499
+ }
500
+ this.expect(TokenType.LParen);
501
+ // `BELIEF (?p)` projects an already-bound Proposition; `BELIEF (s, p, o)`
502
+ // projects a tuple. Only a lone variable followed by `)` is the former.
503
+ if (this.check(TokenType.Variable) && this.peekPast(1)?.type === TokenType.RParen) {
504
+ const proposition = this.parseVariableRef();
505
+ this.expect(TokenType.RParen);
506
+ this.rejectBeliefInRawDialect(start);
507
+ return {
508
+ kind: 'BeliefPattern',
509
+ variable,
510
+ proposition,
511
+ range: { start, end: this.endPos() }
512
+ };
513
+ }
514
+ // `BELIEF (id: ...)` — the operand is the Proposition expression slot, so
515
+ // the same id form that names a Proposition in a pattern names it here
516
+ // (Spec §43.2 / §46.1). Same recognition rule as parsePropositionTuple.
517
+ if (this.isPropositionIdStart()) {
518
+ this.advance(); // id
519
+ this.advance(); // :
520
+ const propositionId = this.parseScalarValue();
521
+ this.expect(TokenType.RParen);
522
+ this.rejectBeliefInRawDialect(start);
523
+ return {
524
+ kind: 'BeliefPattern',
525
+ variable,
526
+ propositionId,
527
+ range: { start, end: this.endPos() }
528
+ };
529
+ }
530
+ // `BELIEF (:p)` is the one spelling a reader might reach for that means
531
+ // nothing: a lone parameter is not a bound variable and not an id
532
+ // reference. Say what the reference form is instead of "expected ','",
533
+ // and recover as if the id form had been written so nothing cascades.
534
+ if (this.check(TokenType.Parameter) && this.peekPast(1)?.type === TokenType.RParen) {
535
+ const param = this.current();
536
+ this.error(`BELIEF (${param.value}) is not a form: name the Proposition by (id: ${param.value}), or bind it first and write BELIEF (?p)`, param);
537
+ const propositionId = this.parseParameterRef();
538
+ this.expect(TokenType.RParen);
539
+ this.rejectBeliefInRawDialect(start);
540
+ return {
541
+ kind: 'BeliefPattern',
542
+ variable,
543
+ propositionId,
544
+ range: { start, end: this.endPos() }
545
+ };
546
+ }
547
+ const subject = this.parseTerm();
548
+ this.expect(TokenType.Comma);
549
+ const predicate = this.parsePredicateAtom();
550
+ this.expect(TokenType.Comma);
551
+ const object = this.parseTerm();
552
+ this.expect(TokenType.RParen);
553
+ this.rejectBeliefInRawDialect(start);
554
+ return {
555
+ kind: 'BeliefPattern',
556
+ variable,
557
+ subject,
558
+ predicate,
559
+ object,
560
+ range: { start, end: this.endPos() }
561
+ };
562
+ }
563
+ /**
564
+ * BELIEF is an Epistemic Projection: virtual, read-only, and derived from a
565
+ * policy. KML excludes it because a Projection can never be a mutation
566
+ * target; EXPORT excludes it because a capsule carries records, not
567
+ * interpretations.
568
+ */
569
+ rejectBeliefInRawDialect(start) {
570
+ if (this.dialect === 'raw') {
571
+ this.diagnostics.push({
572
+ range: { start, end: this.endPos() },
573
+ severity: 'error',
574
+ message: 'BELIEF is a read-only Epistemic Projection and cannot appear in a mutation or export selection',
575
+ code: 'KIP_1001'
576
+ });
577
+ }
578
+ }
579
+ parseFilterClause() {
580
+ const start = this.currentPos();
581
+ this.expect(TokenType.Filter);
582
+ this.expect(TokenType.LParen);
583
+ const expression = this.parseExpression();
584
+ this.expect(TokenType.RParen);
585
+ return {
586
+ kind: 'FilterClause',
587
+ expression,
588
+ range: { start, end: this.endPos() }
589
+ };
590
+ }
591
+ parseNotClause() {
592
+ const start = this.currentPos();
593
+ this.expect(TokenType.Not);
594
+ this.expect(TokenType.LBrace);
595
+ const patterns = this.parseWherePatterns();
596
+ this.expect(TokenType.RBrace);
597
+ return {
598
+ kind: 'NotClause',
599
+ patterns,
600
+ range: { start, end: this.endPos() }
601
+ };
602
+ }
603
+ parseOptionalClause() {
604
+ const start = this.currentPos();
605
+ this.expect(TokenType.Optional);
606
+ this.expect(TokenType.LBrace);
607
+ const patterns = this.parseWherePatterns();
608
+ this.expect(TokenType.RBrace);
609
+ return {
610
+ kind: 'OptionalClause',
611
+ patterns,
612
+ range: { start, end: this.endPos() }
613
+ };
614
+ }
615
+ parseUnionClause() {
616
+ const start = this.currentPos();
617
+ this.expect(TokenType.Union);
618
+ this.expect(TokenType.LBrace);
619
+ const patterns = this.parseWherePatterns();
620
+ this.expect(TokenType.RBrace);
621
+ return {
622
+ kind: 'UnionClause',
623
+ patterns,
624
+ range: { start, end: this.endPos() }
625
+ };
626
+ }
627
+ // ────────────────────────────────────────────────────────────────────
628
+ // Raw semantic tuples
629
+ // ────────────────────────────────────────────────────────────────────
630
+ parsePropositionTuple() {
631
+ const start = this.currentPos();
632
+ this.expect(TokenType.LParen);
633
+ // `(id: ...)` addresses the same slot by record identity. `id` is a field
634
+ // name, not a keyword, so it is matched on its exact lowercase text and
635
+ // only when a `:` follows — `(id, "p", ?o)` is still a triple whose
636
+ // subject happens to be a variable-free term.
637
+ if (this.isPropositionIdStart()) {
638
+ this.advance(); // id
639
+ this.advance(); // :
640
+ const id = this.parseScalarValue();
641
+ this.expect(TokenType.RParen);
642
+ return {
643
+ kind: 'PropositionTuple',
644
+ id,
645
+ range: { start, end: this.endPos() }
646
+ };
647
+ }
648
+ const subject = this.parseTerm();
649
+ this.expect(TokenType.Comma);
650
+ const predicate = this.parseRawPredicateExpression();
651
+ this.expect(TokenType.Comma);
652
+ const object = this.parseTerm();
653
+ this.expect(TokenType.RParen);
654
+ return {
655
+ kind: 'PropositionTuple',
656
+ subject,
657
+ predicate,
658
+ object,
659
+ range: { start, end: this.endPos() }
660
+ };
661
+ }
662
+ /**
663
+ * True when the cursor sits on the `id :` of a `(id: ...)` reference.
664
+ *
665
+ * Field names are case-sensitive, so only the exact spelling `id` counts;
666
+ * `ID` is an ordinary identifier and would not parse as a term anyway.
667
+ */
668
+ isPropositionIdStart() {
669
+ const tok = this.current();
670
+ if (tok.type !== TokenType.Identifier || tok.value !== 'id')
671
+ return false;
672
+ const next = this.peekPast(1);
673
+ // `(id: :p)` lexes the separator as its own colon; `(id:"P")` likewise,
674
+ // because a parameter needs an identifier start after the colon.
675
+ return next?.type === TokenType.Colon;
676
+ }
677
+ parseTerm() {
678
+ const tok = this.current();
679
+ switch (tok.type) {
680
+ case TokenType.Variable:
681
+ return this.parseVariableRef();
682
+ case TokenType.Parameter:
683
+ return this.parseParameterRef();
684
+ case TokenType.LBrace:
685
+ return this.parseObjectPattern();
686
+ case TokenType.LParen:
687
+ return this.parsePropositionTuple();
688
+ case TokenType.String:
689
+ case TokenType.Number:
690
+ case TokenType.Boolean:
691
+ case TokenType.Null:
692
+ return this.parseLiteral();
693
+ default:
694
+ this.error(`Expected a term (variable, parameter, literal, {...} or a tuple) but got '${tok.value}'`, tok);
695
+ throw new ParseAbort();
696
+ }
697
+ }
698
+ /** `predicate_atom = string_literal | parameter | variable` */
699
+ parsePredicateAtom() {
700
+ const tok = this.current();
701
+ if (tok.type === TokenType.String) {
702
+ return this.parseStringLiteral();
703
+ }
704
+ if (tok.type === TokenType.Parameter) {
705
+ return this.parseParameterRef();
706
+ }
707
+ if (tok.type === TokenType.Variable) {
708
+ return this.parseVariableRef();
709
+ }
710
+ this.error(`Expected a predicate (quoted symbol, :parameter or ?variable) but got '${tok.value}'`, tok);
711
+ throw new ParseAbort();
712
+ }
713
+ /**
714
+ * `raw_predicate_expression` — path atoms joined by `|`.
715
+ *
716
+ * Alternation and hop quantifiers are traversal syntax owned by KQL. KML
717
+ * and META spell the same slot as a bare `predicate_atom`, so in the raw
718
+ * dialect anything beyond one plain atom is reported here.
719
+ */
720
+ parseRawPredicateExpression() {
721
+ const start = this.currentPos();
722
+ const atoms = [this.parsePredicatePathAtom()];
723
+ while (this.check(TokenType.Pipe)) {
724
+ const pipe = this.current();
725
+ if (this.dialect === 'raw') {
726
+ this.error('Predicate alternation is a KQL traversal form and is not allowed here', pipe);
727
+ }
728
+ this.advance();
729
+ atoms.push(this.parsePredicatePathAtom());
730
+ }
731
+ return {
732
+ kind: 'RawPredicateExpression',
733
+ atoms,
734
+ range: { start, end: this.endPos() }
735
+ };
736
+ }
737
+ parsePredicatePathAtom() {
738
+ const start = this.currentPos();
739
+ const atom = this.parsePredicateAtom();
740
+ let quantifier;
741
+ if (this.check(TokenType.LBrace)) {
742
+ const brace = this.current();
743
+ if (this.dialect === 'raw') {
744
+ this.error('Path quantifiers are a KQL traversal form and are not allowed here', brace);
586
745
  }
587
- else if (this.check(TokenType.Mode)) {
588
- this.rejectRepeat(modeValue, 'MODE', clause);
746
+ quantifier = this.parsePathQuantifier();
747
+ }
748
+ return {
749
+ kind: 'PredicatePathAtom',
750
+ atom,
751
+ quantifier,
752
+ range: { start, end: this.endPos() }
753
+ };
754
+ }
755
+ parsePathQuantifier() {
756
+ const start = this.currentPos();
757
+ this.expect(TokenType.LBrace);
758
+ const min = this.expectHopCount();
759
+ let max;
760
+ let hasComma = false;
761
+ if (this.match(TokenType.Comma)) {
762
+ hasComma = true;
763
+ if (!this.check(TokenType.RBrace)) {
764
+ max = this.expectHopCount();
765
+ }
766
+ }
767
+ else {
768
+ max = min;
769
+ }
770
+ this.expect(TokenType.RBrace);
771
+ if (max !== undefined && max < min) {
772
+ this.error(`Hop range {${min},${max}} is empty: the maximum is below the minimum`, this.current());
773
+ }
774
+ return {
775
+ kind: 'PathQuantifier',
776
+ min,
777
+ max,
778
+ hasComma,
779
+ range: { start, end: this.endPos() }
780
+ };
781
+ }
782
+ /**
783
+ * A hop count is a plain unsigned integer.
784
+ *
785
+ * `{1.5}`, `{-1}` and `{1e3}` all lex as one number token, so the check is
786
+ * on the token text, not on the parsed value.
787
+ */
788
+ expectHopCount() {
789
+ const tok = this.current();
790
+ if (tok.type !== TokenType.Number || !/^\d+$/.test(tok.value)) {
791
+ this.error(`Expected an unsigned integer hop count but got '${tok.value}'`, tok);
792
+ this.advance();
793
+ return 0;
794
+ }
795
+ const value = Number(tok.value);
796
+ if (value > 65535) {
797
+ this.error(`Hop count ${value} exceeds the 16-bit maximum 65535`, tok);
798
+ }
799
+ this.advance();
800
+ return value;
801
+ }
802
+ // ────────────────────────────────────────────────────────────────────
803
+ // KML — MUTATE
804
+ // ────────────────────────────────────────────────────────────────────
805
+ parseMutateStatement() {
806
+ const leadingComments = this.collectLeadingComments();
807
+ const start = this.currentPos();
808
+ this.expectKeywordWithSpace(TokenType.Mutate);
809
+ this.expect(TokenType.LBrace);
810
+ const clauses = [];
811
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
812
+ const before = this.pos;
813
+ this.skipComments();
814
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
815
+ break;
816
+ // A nested MUTATE is not a smaller transaction, it is a different
817
+ // statement; the grammar forbids it outright.
818
+ if (this.check(TokenType.Mutate)) {
819
+ this.error('MUTATE cannot contain another MUTATE', this.current());
589
820
  this.advance();
590
- modeValue = this.parseStringOrParameterValue('SEARCH MODE');
591
- mode =
592
- modeValue.kind === 'StringLiteral' ? modeValue.parsed : modeValue.name;
821
+ continue;
593
822
  }
594
- else if (this.check(TokenType.Threshold)) {
595
- this.rejectRepeat(threshold, 'THRESHOLD', clause);
596
- threshold = this.parseThresholdClause();
823
+ try {
824
+ clauses.push(this.parseMutationClause());
597
825
  }
598
- else if (this.check(TokenType.Limit)) {
599
- this.rejectRepeat(limit, 'LIMIT', clause);
600
- limit = this.parseLimitClause();
826
+ catch {
827
+ this.recoverToMutationBoundary();
828
+ }
829
+ if (this.pos === before)
830
+ break;
831
+ }
832
+ this.expect(TokenType.RBrace);
833
+ return {
834
+ kind: 'MutateStatement',
835
+ clauses,
836
+ range: { start, end: this.endPos() },
837
+ leadingComments: leadingComments.length ? leadingComments : undefined
838
+ };
839
+ }
840
+ // ────────────────────────────────────────────────────────────────────
841
+ // KML — CREATE / UPSERT / ENSURE / ASSERT
842
+ // ────────────────────────────────────────────────────────────────────
843
+ parseCreateStatement() {
844
+ const leadingComments = this.collectLeadingComments();
845
+ const start = this.currentPos();
846
+ const create = this.expectKeywordWithSpace(TokenType.Create);
847
+ const tok = this.current();
848
+ switch (tok.type) {
849
+ case TokenType.Concept:
850
+ this.expectSecondWord(TokenType.Concept, create);
851
+ return this.parseCreateConceptBody(start, leadingComments);
852
+ case TokenType.Evidence:
853
+ this.expectSecondWord(TokenType.Evidence, create);
854
+ return this.parseRecordCreateBody('CreateEvidenceStatement', start, leadingComments);
855
+ case TokenType.Assertion:
856
+ this.expectSecondWord(TokenType.Assertion, create);
857
+ return this.parseRecordCreateBody('CreateAssertionStatement', start, leadingComments);
858
+ case TokenType.Activity:
859
+ this.expectSecondWord(TokenType.Activity, create);
860
+ return this.parseRecordCreateBody('CreateActivityStatement', start, leadingComments);
861
+ default:
862
+ this.error(`Expected CONCEPT, EVIDENCE, ASSERTION or ACTIVITY after CREATE but got '${tok.value}'`, tok);
863
+ throw new ParseAbort();
864
+ }
865
+ }
866
+ parseCreateConceptBody(start, leadingComments) {
867
+ const handle = this.expectHandle();
868
+ this.expect(TokenType.LBrace);
869
+ const stmt = {
870
+ kind: 'CreateConceptStatement',
871
+ handle,
872
+ setFacets: [],
873
+ range: { start, end: start },
874
+ leadingComments: leadingComments.length ? leadingComments : undefined
875
+ };
876
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
877
+ const before = this.pos;
878
+ this.skipComments();
879
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
880
+ break;
881
+ const tok = this.current();
882
+ switch (tok.type) {
883
+ case TokenType.Type:
884
+ this.rejectRepeat(stmt.type, 'TYPE', tok);
885
+ stmt.type = this.parseTypeClause();
886
+ break;
887
+ case TokenType.Client:
888
+ this.rejectRepeat(stmt.clientKey, 'CLIENT KEY', tok);
889
+ stmt.clientKey = this.parseClientKeyClause();
890
+ break;
891
+ case TokenType.Name:
892
+ this.rejectRepeat(stmt.name, 'NAME', tok);
893
+ stmt.name = this.parseNameClause();
894
+ break;
895
+ case TokenType.Set:
896
+ this.applyCreateSetClause(stmt, tok);
897
+ break;
898
+ default:
899
+ this.error(`Unexpected token '${tok.value}' in CREATE CONCEPT`, tok);
900
+ this.advance();
901
+ }
902
+ if (this.pos === before)
903
+ break;
904
+ }
905
+ this.expect(TokenType.RBrace);
906
+ stmt.range = { start, end: this.endPos() };
907
+ return stmt;
908
+ }
909
+ applyCreateSetClause(stmt, tok) {
910
+ const clause = this.parseSetClause();
911
+ switch (clause.kind) {
912
+ case 'SetFieldsClause':
913
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok);
914
+ stmt.setFields = clause;
915
+ break;
916
+ case 'SetAttributesClause':
917
+ this.rejectRepeat(stmt.setAttributes, 'SET ATTRIBUTES', tok);
918
+ stmt.setAttributes = clause;
919
+ break;
920
+ case 'SetFacetClause':
921
+ stmt.setFacets.push(clause);
922
+ break;
923
+ case 'SetStructuralClause':
924
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok);
925
+ stmt.setStructural = clause;
926
+ break;
927
+ default:
928
+ this.error(`'SET ${clause.kind}' is not allowed in CREATE CONCEPT`, tok);
929
+ }
930
+ }
931
+ /** CREATE EVIDENCE / ASSERTION / ACTIVITY share one clause vocabulary. */
932
+ parseRecordCreateBody(kind, start, leadingComments) {
933
+ const handle = this.expectHandle();
934
+ this.expect(TokenType.LBrace);
935
+ const stmt = {
936
+ kind,
937
+ handle,
938
+ setFacets: [],
939
+ clientKey: undefined,
940
+ setFields: undefined,
941
+ setStructural: undefined,
942
+ range: { start, end: start },
943
+ leadingComments: leadingComments.length ? leadingComments : undefined
944
+ };
945
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
946
+ const before = this.pos;
947
+ this.skipComments();
948
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
949
+ break;
950
+ const tok = this.current();
951
+ if (tok.type === TokenType.Client) {
952
+ this.rejectRepeat(stmt.clientKey, 'CLIENT KEY', tok);
953
+ stmt.clientKey = this.parseClientKeyClause();
954
+ }
955
+ else if (tok.type === TokenType.Set) {
956
+ const clause = this.parseSetClause();
957
+ if (clause.kind === 'SetFieldsClause') {
958
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok);
959
+ stmt.setFields = clause;
960
+ }
961
+ else if (clause.kind === 'SetFacetClause') {
962
+ stmt.setFacets.push(clause);
963
+ }
964
+ else if (clause.kind === 'SetStructuralClause') {
965
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok);
966
+ stmt.setStructural = clause;
967
+ }
968
+ else {
969
+ this.error(`'SET ${clause.kind}' is not allowed in ${kind.replace('Statement', '')}`, tok);
970
+ }
601
971
  }
602
972
  else {
973
+ this.error(`Unexpected token '${tok.value}' in ${kind}`, tok);
974
+ this.advance();
975
+ }
976
+ if (this.pos === before)
603
977
  break;
978
+ }
979
+ this.expect(TokenType.RBrace);
980
+ stmt.range = { start, end: this.endPos() };
981
+ return stmt;
982
+ }
983
+ parseUpsertConcept() {
984
+ const leadingComments = this.collectLeadingComments();
985
+ const start = this.currentPos();
986
+ const upsert = this.expectKeywordWithSpace(TokenType.Upsert);
987
+ this.expectSecondWord(TokenType.Concept, upsert);
988
+ const handle = this.expectHandle();
989
+ this.expect(TokenType.LBrace);
990
+ const stmt = {
991
+ kind: 'UpsertConceptStatement',
992
+ handle,
993
+ setFacets: [],
994
+ unsetFacets: [],
995
+ range: { start, end: start },
996
+ leadingComments: leadingComments.length ? leadingComments : undefined
997
+ };
998
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
999
+ const before = this.pos;
1000
+ this.skipComments();
1001
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
1002
+ break;
1003
+ const tok = this.current();
1004
+ switch (tok.type) {
1005
+ case TokenType.Match:
1006
+ this.rejectRepeat(stmt.match, 'MATCH', tok);
1007
+ stmt.match = this.parseMatchClause();
1008
+ break;
1009
+ case TokenType.Expect:
1010
+ this.rejectRepeat(stmt.expectVersion, 'EXPECT VERSION', tok);
1011
+ stmt.expectVersion = this.parseExpectVersionClause();
1012
+ break;
1013
+ case TokenType.Set: {
1014
+ const clause = this.parseSetClause();
1015
+ if (clause.kind === 'SetFieldsClause') {
1016
+ this.rejectRepeat(stmt.setFields, 'SET FIELDS', tok);
1017
+ stmt.setFields = clause;
1018
+ }
1019
+ else if (clause.kind === 'SetAttributesClause') {
1020
+ this.rejectRepeat(stmt.setAttributes, 'SET ATTRIBUTES', tok);
1021
+ stmt.setAttributes = clause;
1022
+ }
1023
+ else if (clause.kind === 'SetFacetClause') {
1024
+ stmt.setFacets.push(clause);
1025
+ }
1026
+ else if (clause.kind === 'SetStructuralClause') {
1027
+ this.rejectRepeat(stmt.setStructural, 'SET STRUCTURAL', tok);
1028
+ stmt.setStructural = clause;
1029
+ }
1030
+ else {
1031
+ this.error(`'SET RETENTION' is not a clause of UPSERT CONCEPT`, tok);
1032
+ }
1033
+ break;
1034
+ }
1035
+ case TokenType.Unset: {
1036
+ const clause = this.parseUnsetClause();
1037
+ if (clause.kind === 'UnsetAttributesClause') {
1038
+ this.rejectRepeat(stmt.unsetAttributes, 'UNSET ATTRIBUTES', tok);
1039
+ stmt.unsetAttributes = clause;
1040
+ }
1041
+ else if (clause.kind === 'UnsetStructuralClause') {
1042
+ this.rejectRepeat(stmt.unsetStructural, 'UNSET STRUCTURAL', tok);
1043
+ stmt.unsetStructural = clause;
1044
+ }
1045
+ else {
1046
+ stmt.unsetFacets.push(clause);
1047
+ }
1048
+ break;
1049
+ }
1050
+ default:
1051
+ this.error(`Unexpected token '${tok.value}' in UPSERT CONCEPT`, tok);
1052
+ this.advance();
604
1053
  }
605
- // A sub-parser that rejects its first token reports and returns
606
- // without consuming it, so a loop keyed on that token would spin
607
- // forever building diagnostics. Stop as soon as nothing moved.
608
1054
  if (this.pos === before)
609
1055
  break;
610
1056
  }
1057
+ this.expect(TokenType.RBrace);
1058
+ stmt.range = { start, end: this.endPos() };
1059
+ return stmt;
1060
+ }
1061
+ parseEnsureProposition() {
1062
+ const leadingComments = this.collectLeadingComments();
1063
+ const start = this.currentPos();
1064
+ const ensure = this.expectKeywordWithSpace(TokenType.Ensure);
1065
+ this.expectSecondWord(TokenType.Proposition, ensure);
1066
+ const handle = this.check(TokenType.Variable)
1067
+ ? this.parseVariableRef()
1068
+ : undefined;
1069
+ this.dialect = 'raw';
1070
+ const tuple = this.parsePropositionTuple();
1071
+ const expectVersion = this.check(TokenType.Expect)
1072
+ ? this.parseExpectVersionClause()
1073
+ : undefined;
1074
+ return {
1075
+ kind: 'EnsurePropositionStatement',
1076
+ handle,
1077
+ tuple,
1078
+ expectVersion,
1079
+ range: { start, end: this.endPos() },
1080
+ leadingComments: leadingComments.length ? leadingComments : undefined
1081
+ };
1082
+ }
1083
+ parseAssertStatement() {
1084
+ const leadingComments = this.collectLeadingComments();
1085
+ const start = this.currentPos();
1086
+ this.expectKeywordWithSpace(TokenType.Assert);
1087
+ const handle = this.check(TokenType.Variable)
1088
+ ? this.parseVariableRef()
1089
+ : undefined;
1090
+ this.dialect = 'raw';
1091
+ const tuple = this.parsePropositionTuple();
1092
+ const assignments = this.parseAssignmentObject();
1093
+ let superseding;
1094
+ if (this.match(TokenType.Superseding)) {
1095
+ superseding = this.parseTargetRef();
1096
+ }
611
1097
  return {
612
- kind: 'SearchStatement',
613
- searchTarget,
614
- term,
615
- termValue,
616
- withType,
617
- withTypeValue,
618
- mode,
619
- modeValue,
620
- threshold,
621
- limit,
622
- range: { start, end: this.currentPos() },
623
- leadingComments: comments.length > 0 ? comments : undefined
1098
+ kind: 'AssertStatement',
1099
+ handle,
1100
+ tuple,
1101
+ assignments,
1102
+ superseding,
1103
+ range: { start, end: this.endPos() },
1104
+ leadingComments: leadingComments.length ? leadingComments : undefined
624
1105
  };
625
1106
  }
626
1107
  // ────────────────────────────────────────────────────────────────────
627
- // EXPORT
1108
+ // KML — clause vocabulary
628
1109
  // ────────────────────────────────────────────────────────────────────
629
- parseExportStatement() {
1110
+ parseTypeClause() {
630
1111
  const start = this.currentPos();
631
- const comments = this.collectLeadingComments();
632
- this.expectKeywordWithSpace(TokenType.Export);
633
- const target = this.expectVariable();
634
- const where = this.parseWhereClause();
635
- let limit;
636
- let cursor;
637
- if (this.check(TokenType.Limit)) {
638
- limit = this.parseLimitClause();
639
- }
640
- if (this.check(TokenType.Cursor)) {
641
- cursor = this.parseCursorClause();
642
- }
1112
+ this.expect(TokenType.Type);
1113
+ const value = this.parseSchemaSymbol();
1114
+ return { kind: 'TypeClause', value, range: { start, end: this.endPos() } };
1115
+ }
1116
+ parseClientKeyClause() {
1117
+ const start = this.currentPos();
1118
+ const client = this.expect(TokenType.Client);
1119
+ this.expectSecondWord(TokenType.Key, client);
1120
+ const value = this.parseScalarValue();
643
1121
  return {
644
- kind: 'ExportStatement',
645
- target,
646
- where,
647
- limit,
648
- cursor,
649
- range: { start, end: this.currentPos() },
650
- leadingComments: comments.length > 0 ? comments : undefined
1122
+ kind: 'ClientKeyClause',
1123
+ value,
1124
+ range: { start, end: this.endPos() }
651
1125
  };
652
1126
  }
653
- // ────────────────────────────────────────────────────────────────────
654
- // WHERE clause and patterns
655
- // ────────────────────────────────────────────────────────────────────
656
- parseWhereClause() {
1127
+ parseNameClause() {
657
1128
  const start = this.currentPos();
658
- this.expect(TokenType.Where);
659
- this.expect(TokenType.LBrace);
660
- const patterns = this.parseWherePatterns();
661
- this.expect(TokenType.RBrace);
1129
+ this.expect(TokenType.Name);
1130
+ const value = this.parseScalarValue();
1131
+ return { kind: 'NameClause', value, range: { start, end: this.endPos() } };
1132
+ }
1133
+ parseMatchClause() {
1134
+ const start = this.currentPos();
1135
+ this.expect(TokenType.Match);
1136
+ const pattern = this.parseObjectPattern();
662
1137
  return {
663
- kind: 'WhereClause',
664
- patterns,
665
- range: { start, end: this.currentPos() }
1138
+ kind: 'MatchClause',
1139
+ pattern,
1140
+ range: { start, end: this.endPos() }
666
1141
  };
667
1142
  }
668
- parseWherePatterns() {
669
- const patterns = [];
670
- this.skipComments();
1143
+ /** Dispatches every `SET ...` form; callers reject the ones they disallow. */
1144
+ parseSetClause() {
1145
+ const start = this.currentPos();
1146
+ const set = this.expect(TokenType.Set);
1147
+ const tok = this.current();
1148
+ switch (tok.type) {
1149
+ case TokenType.Fields: {
1150
+ this.expectSecondWord(TokenType.Fields, set);
1151
+ const assignments = this.parseAssignmentObject();
1152
+ return {
1153
+ kind: 'SetFieldsClause',
1154
+ assignments,
1155
+ range: { start, end: this.endPos() }
1156
+ };
1157
+ }
1158
+ case TokenType.Attributes: {
1159
+ this.expectSecondWord(TokenType.Attributes, set);
1160
+ const assignments = this.parseAssignmentObject();
1161
+ return {
1162
+ kind: 'SetAttributesClause',
1163
+ assignments,
1164
+ range: { start, end: this.endPos() }
1165
+ };
1166
+ }
1167
+ case TokenType.Facet: {
1168
+ this.expectSecondWord(TokenType.Facet, set);
1169
+ const facet = this.parseSchemaSymbol();
1170
+ const assignments = this.parseAssignmentObject();
1171
+ return {
1172
+ kind: 'SetFacetClause',
1173
+ facet,
1174
+ assignments,
1175
+ range: { start, end: this.endPos() }
1176
+ };
1177
+ }
1178
+ case TokenType.Structural: {
1179
+ this.expectSecondWord(TokenType.Structural, set);
1180
+ return this.parseSetStructuralBody(start);
1181
+ }
1182
+ case TokenType.Retention:
1183
+ return { kind: 'SetRetentionMarker' };
1184
+ default:
1185
+ this.error(`Expected FIELDS, ATTRIBUTES, FACET, STRUCTURAL or RETENTION after SET but got '${tok.value}'`, tok);
1186
+ throw new ParseAbort();
1187
+ }
1188
+ }
1189
+ parseSetStructuralBody(start) {
1190
+ this.expect(TokenType.LBrace);
1191
+ const assignments = [];
671
1192
  while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
672
1193
  const before = this.pos;
673
1194
  this.skipComments();
674
- if (this.check(TokenType.RBrace))
1195
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
675
1196
  break;
676
- const pattern = this.parseWherePattern();
677
- if (pattern)
678
- patterns.push(pattern);
679
- this.skipComments();
680
- // A sub-parser that rejects its first token reports and returns
681
- // without consuming it, so a loop keyed on that token would spin
682
- // forever building diagnostics. Stop as soon as nothing moved.
1197
+ assignments.push(this.parseStructuralAssignment());
683
1198
  if (this.pos === before)
684
1199
  break;
685
1200
  }
686
- return patterns;
687
- }
688
- parseWherePattern() {
689
- this.skipComments();
690
- const tok = this.current();
691
- if (tok.type === TokenType.Filter) {
692
- return this.parseFilterClause();
693
- }
694
- if (tok.type === TokenType.Not) {
695
- return this.parseNotClause();
696
- }
697
- if (tok.type === TokenType.Optional) {
698
- return this.parseOptionalClause();
699
- }
700
- if (tok.type === TokenType.Union) {
701
- return this.parseUnionClause();
702
- }
703
- // Variable: could be concept pattern or proposition pattern
704
- if (tok.type === TokenType.Variable) {
705
- return this.parseVariableLeadingPattern();
706
- }
707
- // Opening ( = proposition pattern without variable binding
708
- if (tok.type === TokenType.LParen) {
709
- return this.parsePropositionPatternBody(undefined);
710
- }
711
- this.error(`Unexpected token '${tok.value}' in WHERE clause`, tok);
712
- this.advance();
713
- return null;
714
- }
715
- parseVariableLeadingPattern() {
716
- // ?var could be followed by:
717
- // { ... } => concept pattern
718
- // ( ... ) => proposition pattern
719
- const start = this.currentPos();
720
- const variable = this.expectVariable();
721
- if (this.check(TokenType.LBrace)) {
722
- return this.parseConceptPatternBody(variable, start);
723
- }
724
- if (this.check(TokenType.LParen)) {
725
- return this.parsePropositionPatternBody(variable, start);
726
- }
727
- // Just a variable reference as a standalone concept pattern without matcher
728
- // This occurs in WHERE like: ?drug {type: "Drug"}
729
- this.error(`Expected '{' or '(' after variable '${variable}' in WHERE clause`, this.current());
730
- return {
731
- kind: 'ConceptPattern',
732
- variable,
733
- matcher: {
734
- kind: 'ConceptMatcher',
735
- entries: [],
736
- range: { start, end: this.currentPos() }
737
- },
738
- range: { start, end: this.currentPos() }
739
- };
740
- }
741
- parseConceptPatternBody(variable, start) {
742
- const matcher = this.parseConceptMatcher();
743
- return {
744
- kind: 'ConceptPattern',
745
- variable,
746
- matcher,
747
- range: { start, end: this.currentPos() }
748
- };
749
- }
750
- parseConceptMatcher() {
751
- const start = this.currentPos();
752
- const brace = this.expect(TokenType.LBrace);
753
- const seen = { trailingComma: false };
754
- const entries = this.parseObjectEntries(seen);
755
- if (seen.trailingComma) {
756
- this.error(`A concept matcher takes no trailing comma`, brace);
757
- }
758
1201
  this.expect(TokenType.RBrace);
759
1202
  return {
760
- kind: 'ConceptMatcher',
761
- entries,
762
- range: { start, end: this.currentPos() }
1203
+ kind: 'SetStructuralClause',
1204
+ assignments,
1205
+ range: { start, end: this.endPos() }
763
1206
  };
764
1207
  }
765
- parsePropositionPatternBody(variable, start = this.currentPos()) {
1208
+ parseStructuralAssignment() {
1209
+ const start = this.currentPos();
766
1210
  this.expect(TokenType.LParen);
767
- if (this.isIdMatcherStart()) {
768
- const id = this.parseIdMatcherValue();
769
- this.expect(TokenType.RParen);
770
- return {
771
- kind: 'PropositionPattern',
772
- variable,
773
- id,
774
- range: { start, end: this.currentPos() }
775
- };
776
- }
777
- const subject = this.parsePropositionEndpoint();
778
- this.expect(TokenType.Comma);
779
- const predicate = this.parsePredicateExpr();
1211
+ const field = this.parseSchemaSymbol();
780
1212
  this.expect(TokenType.Comma);
781
- const object = this.parsePropositionEndpoint();
1213
+ const value = this.parseMutationValue();
782
1214
  this.expect(TokenType.RParen);
1215
+ const options = this.check(TokenType.LBrace)
1216
+ ? this.parseObjectLiteral()
1217
+ : undefined;
783
1218
  return {
784
- kind: 'PropositionPattern',
785
- variable,
786
- subject,
787
- predicate,
788
- object,
789
- range: { start, end: this.currentPos() }
1219
+ kind: 'StructuralAssignment',
1220
+ field,
1221
+ value,
1222
+ options,
1223
+ range: { start, end: this.endPos() }
790
1224
  };
791
1225
  }
792
- parsePropositionEndpoint() {
793
- // Could be: ?var, ?var {...}, ?var (...), {...}, or nested (...)
794
- if (this.check(TokenType.Variable)) {
795
- const start = this.currentPos();
796
- const name = this.expectVariable();
797
- if (this.check(TokenType.LBrace)) {
798
- return this.parseConceptPatternBody(name, start);
799
- }
800
- if (this.check(TokenType.LParen)) {
801
- return this.parsePropositionPatternBody(name, start);
802
- }
803
- return {
804
- kind: 'VariableRef',
805
- name,
806
- range: { start, end: this.currentPos() }
807
- };
808
- }
809
- if (this.check(TokenType.LBrace)) {
810
- const start = this.currentPos();
811
- const matcher = this.parseConceptMatcher();
1226
+ parseUnsetClause() {
1227
+ const start = this.currentPos();
1228
+ const unset = this.expect(TokenType.Unset);
1229
+ const tok = this.current();
1230
+ if (tok.type === TokenType.Attributes) {
1231
+ this.expectSecondWord(TokenType.Attributes, unset);
1232
+ const fields = this.parseUnsetFieldSet();
812
1233
  return {
813
- kind: 'ConceptPattern',
814
- matcher,
815
- range: { start, end: this.currentPos() }
1234
+ kind: 'UnsetAttributesClause',
1235
+ fields,
1236
+ range: { start, end: this.endPos() }
816
1237
  };
817
1238
  }
818
- if (this.check(TokenType.LParen)) {
819
- return this.parsePropositionPatternBody(undefined);
820
- }
821
- this.error(`Expected variable, concept pattern, or proposition pattern`, this.current());
822
- const start = this.currentPos();
823
- return {
824
- kind: 'VariableRef',
825
- name: '?unknown',
826
- range: { start, end: start }
827
- };
828
- }
829
- parsePredicateExpr() {
830
- const start = this.currentPos();
831
- if (this.check(TokenType.Variable)) {
832
- const pred = this.parsePredicateVariable();
833
- if (this.check(TokenType.LBrace)) {
834
- this.error(`Predicate variables cannot use hop ranges; use a quoted predicate literal for path traversal`, this.current());
835
- this.parseHopRange();
836
- }
837
- if (this.check(TokenType.Pipe)) {
838
- this.error(`Predicate variables cannot be used in predicate alternations`, this.current());
839
- while (this.match(TokenType.Pipe)) {
840
- if (this.check(TokenType.String)) {
841
- this.parsePredicateLiteral();
842
- }
843
- else if (this.check(TokenType.Variable)) {
844
- this.parsePredicateVariable();
845
- }
846
- else {
847
- break;
848
- }
849
- }
850
- }
1239
+ if (tok.type === TokenType.Facet) {
1240
+ this.expectSecondWord(TokenType.Facet, unset);
1241
+ const facet = this.parseSchemaSymbol();
1242
+ const fields = this.parseUnsetFieldSet();
851
1243
  return {
852
- ...pred,
853
- range: { start, end: this.currentPos() }
1244
+ kind: 'UnsetFacetClause',
1245
+ facet,
1246
+ fields,
1247
+ range: { start, end: this.endPos() }
854
1248
  };
855
1249
  }
856
- const first = this.parsePredicateLiteral();
857
- // Check for alternation: "pred1" | "pred2"
858
- if (this.check(TokenType.Pipe)) {
859
- const predicates = [first];
860
- while (this.match(TokenType.Pipe)) {
861
- predicates.push(this.parsePredicateLiteral());
1250
+ if (tok.type === TokenType.Structural) {
1251
+ // Every SET has an UNSET: an entry is the SET STRUCTURAL entry without
1252
+ // its options object (Spec §17.5).
1253
+ this.expectSecondWord(TokenType.Structural, unset);
1254
+ this.expect(TokenType.LBrace);
1255
+ const removals = [];
1256
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1257
+ const before = this.pos;
1258
+ this.skipComments();
1259
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
1260
+ break;
1261
+ removals.push(this.parseStructuralRemoval());
1262
+ if (this.pos === before)
1263
+ break;
862
1264
  }
1265
+ this.expect(TokenType.RBrace);
863
1266
  return {
864
- kind: 'PredicateAlternation',
865
- predicates,
866
- range: { start, end: this.currentPos() }
1267
+ kind: 'UnsetStructuralClause',
1268
+ removals,
1269
+ range: { start, end: this.endPos() }
867
1270
  };
868
1271
  }
869
- return first;
870
- }
871
- parsePredicateVariable() {
872
- const start = this.currentPos();
873
- const name = this.expectVariable();
874
- return {
875
- kind: 'PredicateVariable',
876
- name,
877
- range: { start, end: this.currentPos() }
878
- };
1272
+ this.error(`Expected ATTRIBUTES, FACET or STRUCTURAL after UNSET but got '${tok.value}'`, tok);
1273
+ throw new ParseAbort();
879
1274
  }
880
- parsePredicateLiteral() {
1275
+ parseStructuralRemoval() {
881
1276
  const start = this.currentPos();
882
- const value = this.expectStringValue();
883
- // Check for hop range: {m,n} {m,} {m}
884
- let hopRange;
1277
+ this.expect(TokenType.LParen);
1278
+ const field = this.parseSchemaSymbol();
1279
+ this.expect(TokenType.Comma);
1280
+ const value = this.parseMutationValue();
1281
+ this.expect(TokenType.RParen);
885
1282
  if (this.check(TokenType.LBrace)) {
886
- hopRange = this.parseHopRange();
1283
+ this.error('UNSET STRUCTURAL removes a reference by (field, target); it takes no options object', this.current());
887
1284
  }
888
1285
  return {
889
- kind: 'PredicateLiteral',
1286
+ kind: 'StructuralRemoval',
1287
+ field,
890
1288
  value,
891
- hopRange,
892
- range: { start, end: this.currentPos() }
1289
+ range: { start, end: this.endPos() }
893
1290
  };
894
1291
  }
895
- parseHopRange() {
896
- const start = this.currentPos();
1292
+ parseUnsetFieldSet() {
897
1293
  this.expect(TokenType.LBrace);
898
- const min = this.expectHopCount();
899
- let max;
900
- if (this.match(TokenType.Comma)) {
901
- if (this.check(TokenType.Number)) {
902
- max = this.expectHopCount();
903
- }
904
- // else: {m,} means unbounded
905
- }
906
- else {
907
- max = min; // {m} means exactly m
1294
+ const fields = [];
1295
+ if (!this.check(TokenType.RBrace)) {
1296
+ do {
1297
+ if (this.check(TokenType.RBrace))
1298
+ break;
1299
+ const start = this.currentPos();
1300
+ const { key, isQuoted } = this.expectKeyWithQuoting();
1301
+ fields.push({
1302
+ kind: 'UnsetField',
1303
+ name: key,
1304
+ isQuoted,
1305
+ range: { start, end: this.endPos() }
1306
+ });
1307
+ } while (this.match(TokenType.Comma));
908
1308
  }
909
1309
  this.expect(TokenType.RBrace);
910
- return {
911
- kind: 'HopRange',
912
- min,
913
- max,
914
- range: { start, end: this.currentPos() }
915
- };
916
- }
917
- /**
918
- * Reads one bound of a `{m,n}` hop quantifier.
919
- *
920
- * A hop count is a plain 16-bit integer — no sign, no decimal point, no
921
- * exponent. `"p"{1e9,}` is not an enormous traversal, it is a typo, and
922
- * accepting it would hand the engine a bound it cannot honour.
923
- */
924
- expectHopCount() {
925
- const tok = this.current();
926
- if (tok.type !== TokenType.Number || !/^[0-9]+$/.test(tok.value)) {
927
- this.error(`Expected a whole number in a hop range`, tok);
928
- this.advance();
929
- return 0;
930
- }
931
- const value = Number(tok.value);
932
- if (value > 0xffff) {
933
- this.error(`Hop count ${tok.value} exceeds the maximum of 65535`, tok);
934
- }
935
- this.advance();
936
- return value;
937
- }
938
- parseFilterClause() {
939
- const start = this.currentPos();
940
- this.expect(TokenType.Filter);
941
- this.expect(TokenType.LParen);
942
- const expression = this.parseExpression();
943
- this.expect(TokenType.RParen);
944
- return {
945
- kind: 'FilterClause',
946
- expression,
947
- range: { start, end: this.currentPos() }
948
- };
1310
+ return fields;
949
1311
  }
950
- parseNotClause() {
1312
+ parseExpectVersionClause() {
951
1313
  const start = this.currentPos();
952
- this.expect(TokenType.Not);
953
- this.expect(TokenType.LBrace);
954
- const patterns = this.parseWherePatterns();
955
- this.expect(TokenType.RBrace);
1314
+ const expect = this.expect(TokenType.Expect);
1315
+ this.expectSecondWord(TokenType.Version, expect);
1316
+ const value = this.parseScalarValue();
956
1317
  return {
957
- kind: 'NotClause',
958
- patterns,
959
- range: { start, end: this.currentPos() }
1318
+ kind: 'ExpectVersionClause',
1319
+ value,
1320
+ range: { start, end: this.endPos() }
960
1321
  };
961
1322
  }
962
- parseOptionalClause() {
1323
+ parseExpectStateClause() {
963
1324
  const start = this.currentPos();
964
- this.expect(TokenType.Optional);
965
- this.expect(TokenType.LBrace);
966
- const patterns = this.parseWherePatterns();
967
- this.expect(TokenType.RBrace);
1325
+ const expect = this.expect(TokenType.Expect);
1326
+ this.expectSecondWord(TokenType.State, expect);
1327
+ const value = this.parseScalarValue();
968
1328
  return {
969
- kind: 'OptionalClause',
970
- patterns,
971
- range: { start, end: this.currentPos() }
1329
+ kind: 'ExpectStateClause',
1330
+ value,
1331
+ range: { start, end: this.endPos() }
972
1332
  };
973
1333
  }
974
- parseUnionClause() {
1334
+ // ────────────────────────────────────────────────────────────────────
1335
+ // KML — UPDATE
1336
+ // ────────────────────────────────────────────────────────────────────
1337
+ parseUpdateStatement() {
1338
+ const leadingComments = this.collectLeadingComments();
975
1339
  const start = this.currentPos();
976
- this.expect(TokenType.Union);
977
- this.expect(TokenType.LBrace);
978
- const patterns = this.parseWherePatterns();
979
- this.expect(TokenType.RBrace);
1340
+ this.expectKeywordWithSpace(TokenType.Update);
1341
+ const target = this.parseTargetRef();
1342
+ const expectVersion = this.check(TokenType.Expect)
1343
+ ? this.parseExpectVersionClause()
1344
+ : undefined;
1345
+ const actions = [];
1346
+ for (;;) {
1347
+ if (this.check(TokenType.Set)) {
1348
+ const clause = this.parseSetClause();
1349
+ if (clause.kind === 'SetRetentionMarker') {
1350
+ this.error('SET RETENTION is its own statement, not an UPDATE action', this.current());
1351
+ break;
1352
+ }
1353
+ actions.push(clause);
1354
+ }
1355
+ else if (this.check(TokenType.Unset)) {
1356
+ actions.push(this.parseUnsetClause());
1357
+ }
1358
+ else {
1359
+ break;
1360
+ }
1361
+ }
1362
+ if (actions.length === 0) {
1363
+ this.error('UPDATE requires at least one SET or UNSET action', this.current());
1364
+ }
1365
+ // WHERE binds a ?variable target; a direct :id / "id" target already names
1366
+ // the element and may omit it, exactly as ARCHIVE / TOMBSTONE / PURGE /
1367
+ // SET RETENTION / RETRACT ASSERTION do (Spec §58). Whether a bare
1368
+ // ?variable is bound is semantic — inside MUTATE it may be a local handle.
1369
+ let where;
1370
+ if (this.check(TokenType.Where)) {
1371
+ this.expectKeywordWithSpace(TokenType.Where);
1372
+ this.dialect = 'raw';
1373
+ where = this.parseWhereClause();
1374
+ }
1375
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
980
1376
  return {
981
- kind: 'UnionClause',
982
- patterns,
983
- range: { start, end: this.currentPos() }
1377
+ kind: 'UpdateStatement',
1378
+ target,
1379
+ expectVersion,
1380
+ actions,
1381
+ where,
1382
+ limit,
1383
+ range: { start, end: this.endPos() },
1384
+ leadingComments: leadingComments.length ? leadingComments : undefined
984
1385
  };
985
1386
  }
986
1387
  // ────────────────────────────────────────────────────────────────────
987
- // SET ATTRIBUTES / SET PROPOSITIONS / WITH METADATA
1388
+ // KML lifecycle and correction
988
1389
  // ────────────────────────────────────────────────────────────────────
989
- parseSetAttributesBody(start) {
990
- this.expect(TokenType.LBrace);
991
- const entries = this.parseObjectEntries();
992
- this.expect(TokenType.RBrace);
1390
+ parseRetractAssertion() {
1391
+ const leadingComments = this.collectLeadingComments();
1392
+ const start = this.currentPos();
1393
+ const retract = this.expectKeywordWithSpace(TokenType.Retract);
1394
+ this.expectSecondWord(TokenType.Assertion, retract);
1395
+ const target = this.parseTargetRef();
1396
+ let where;
1397
+ if (this.check(TokenType.Where)) {
1398
+ this.expectKeywordWithSpace(TokenType.Where);
1399
+ this.dialect = 'raw';
1400
+ where = this.parseWhereClause();
1401
+ }
1402
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1403
+ const expectState = this.check(TokenType.Expect)
1404
+ ? this.parseExpectStateClause()
1405
+ : undefined;
993
1406
  return {
994
- kind: 'SetAttributes',
995
- entries,
996
- range: { start, end: this.currentPos() }
1407
+ kind: 'RetractAssertionStatement',
1408
+ target,
1409
+ where,
1410
+ limit,
1411
+ expectState,
1412
+ range: { start, end: this.endPos() },
1413
+ leadingComments: leadingComments.length ? leadingComments : undefined
997
1414
  };
998
1415
  }
999
- parseSetMetadataBody(start) {
1000
- this.expect(TokenType.LBrace);
1001
- const entries = this.parseObjectEntries();
1002
- this.expect(TokenType.RBrace);
1416
+ parseSupersedeAssertion() {
1417
+ const leadingComments = this.collectLeadingComments();
1418
+ const start = this.currentPos();
1419
+ const supersede = this.expectKeywordWithSpace(TokenType.Supersede);
1420
+ this.expectSecondWord(TokenType.Assertion, supersede);
1421
+ const target = this.parseTargetRef();
1422
+ this.expect(TokenType.By);
1423
+ const by = this.parseTargetRef();
1424
+ const expectState = this.check(TokenType.Expect)
1425
+ ? this.parseExpectStateClause()
1426
+ : undefined;
1003
1427
  return {
1004
- kind: 'SetMetadata',
1005
- entries,
1006
- range: { start, end: this.currentPos() }
1428
+ kind: 'SupersedeAssertionStatement',
1429
+ target,
1430
+ by,
1431
+ expectState,
1432
+ range: { start, end: this.endPos() },
1433
+ leadingComments: leadingComments.length ? leadingComments : undefined
1007
1434
  };
1008
1435
  }
1009
- parseSetPropositionsBody(start) {
1010
- this.expect(TokenType.LBrace);
1011
- const items = [];
1012
- this.skipComments();
1013
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1014
- const before = this.pos;
1015
- this.skipComments();
1016
- if (this.check(TokenType.RBrace))
1017
- break;
1018
- items.push(this.parsePropositionItem());
1019
- this.skipComments();
1020
- // Items are juxtaposed, but a separating comma — including a trailing
1021
- // one — is tolerated. Generated KML reaches for it constantly, and the
1022
- // reference grammar accepts it.
1023
- this.match(TokenType.Comma);
1024
- this.skipComments();
1025
- // A sub-parser that rejects its first token reports and returns
1026
- // without consuming it, so a loop keyed on that token would spin
1027
- // forever building diagnostics. Stop as soon as nothing moved.
1028
- if (this.pos === before)
1436
+ parseCorrectEvidence() {
1437
+ const leadingComments = this.collectLeadingComments();
1438
+ const start = this.currentPos();
1439
+ const correct = this.expectKeywordWithSpace(TokenType.Correct);
1440
+ this.expectSecondWord(TokenType.Evidence, correct);
1441
+ const target = this.parseTargetRef();
1442
+ this.expect(TokenType.By);
1443
+ const by = this.parseTargetRef();
1444
+ const expectState = this.check(TokenType.Expect)
1445
+ ? this.parseExpectStateClause()
1446
+ : undefined;
1447
+ return {
1448
+ kind: 'CorrectEvidenceStatement',
1449
+ target,
1450
+ by,
1451
+ expectState,
1452
+ range: { start, end: this.endPos() },
1453
+ leadingComments: leadingComments.length ? leadingComments : undefined
1454
+ };
1455
+ }
1456
+ parseTransitionActivity() {
1457
+ const leadingComments = this.collectLeadingComments();
1458
+ const start = this.currentPos();
1459
+ const transition = this.expectKeywordWithSpace(TokenType.Transition);
1460
+ this.expectSecondWord(TokenType.Activity, transition);
1461
+ const target = this.parseTargetRef();
1462
+ this.expect(TokenType.To);
1463
+ const to = this.parseScalarValue();
1464
+ // Terminal outputs and ended_at may be finalized in the same statement
1465
+ // that moves the Activity to its terminal state.
1466
+ const finalize = [];
1467
+ while (this.check(TokenType.Set)) {
1468
+ const clause = this.parseSetClause();
1469
+ if (clause.kind === 'SetFieldsClause' || clause.kind === 'SetStructuralClause') {
1470
+ finalize.push(clause);
1471
+ }
1472
+ else {
1473
+ this.error('TRANSITION ACTIVITY accepts only SET FIELDS and SET STRUCTURAL', this.current());
1029
1474
  break;
1475
+ }
1030
1476
  }
1031
- this.expect(TokenType.RBrace);
1477
+ const expectState = this.check(TokenType.Expect)
1478
+ ? this.parseExpectStateClause()
1479
+ : undefined;
1032
1480
  return {
1033
- kind: 'SetPropositions',
1034
- items,
1035
- range: { start, end: this.currentPos() }
1481
+ kind: 'TransitionActivityStatement',
1482
+ target,
1483
+ to,
1484
+ finalize,
1485
+ expectState,
1486
+ range: { start, end: this.endPos() },
1487
+ leadingComments: leadingComments.length ? leadingComments : undefined
1036
1488
  };
1037
1489
  }
1038
- parsePropositionItem() {
1490
+ // ────────────────────────────────────────────────────────────────────
1491
+ // KML — retention and removal
1492
+ // ────────────────────────────────────────────────────────────────────
1493
+ parseSetRetention() {
1494
+ const leadingComments = this.collectLeadingComments();
1039
1495
  const start = this.currentPos();
1040
- this.expect(TokenType.LParen);
1041
- const predicate = this.expectStringValue();
1042
- this.expect(TokenType.Comma);
1043
- const target = this.parsePropositionEndpoint();
1044
- this.expect(TokenType.RParen);
1045
- let metadata;
1046
- if (this.check(TokenType.With)) {
1047
- metadata = this.parseWithMetadata();
1496
+ const set = this.expectKeywordWithSpace(TokenType.Set);
1497
+ this.expectSecondWord(TokenType.Retention, set);
1498
+ const target = this.parseTargetRef();
1499
+ const assignments = this.parseAssignmentObject();
1500
+ let where;
1501
+ if (this.check(TokenType.Where)) {
1502
+ this.expectKeywordWithSpace(TokenType.Where);
1503
+ this.dialect = 'raw';
1504
+ where = this.parseWhereClause();
1048
1505
  }
1506
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1507
+ const expectVersion = this.check(TokenType.Expect)
1508
+ ? this.parseExpectVersionClause()
1509
+ : undefined;
1049
1510
  return {
1050
- kind: 'PropositionItem',
1051
- predicate,
1511
+ kind: 'SetRetentionStatement',
1052
1512
  target,
1053
- metadata,
1054
- range: { start, end: this.currentPos() }
1513
+ assignments,
1514
+ where,
1515
+ limit,
1516
+ expectVersion,
1517
+ range: { start, end: this.endPos() },
1518
+ leadingComments: leadingComments.length ? leadingComments : undefined
1055
1519
  };
1056
1520
  }
1057
- isIdMatcherStart() {
1058
- if (!this.isIdKeyToken(this.current()))
1059
- return false;
1060
- const next = this.peekPast(this.pos + 1);
1061
- // `(id: "...")` may be written with a comment between the key and the
1062
- // colon; comments are trivia everywhere else, so they are here too.
1063
- return (next?.type === TokenType.Colon ||
1064
- (next?.type === TokenType.Parameter && next.value.startsWith(':')));
1521
+ parseArchiveStatement() {
1522
+ const { start, target, where, limit, expectState, leadingComments } = this.parseRemovalBody(TokenType.Archive);
1523
+ return {
1524
+ kind: 'ArchiveStatement',
1525
+ target,
1526
+ where,
1527
+ limit,
1528
+ expectState,
1529
+ range: { start, end: this.endPos() },
1530
+ leadingComments
1531
+ };
1065
1532
  }
1066
- /** The first non-comment token at or after `i`. */
1067
- peekPast(i) {
1068
- while (this.tokens[i]?.type === TokenType.Comment)
1069
- i++;
1070
- return this.tokens[i];
1533
+ parseTombstoneStatement() {
1534
+ const { start, target, where, limit, expectState, leadingComments } = this.parseRemovalBody(TokenType.Tombstone);
1535
+ return {
1536
+ kind: 'TombstoneStatement',
1537
+ target,
1538
+ where,
1539
+ limit,
1540
+ expectState,
1541
+ range: { start, end: this.endPos() },
1542
+ leadingComments
1543
+ };
1071
1544
  }
1072
- parseIdMatcherValue() {
1073
- const keyTok = this.current();
1074
- if (!this.isIdKeyToken(keyTok)) {
1075
- this.error(`Expected id matcher key but got '${keyTok.value}'`, keyTok);
1545
+ /** ARCHIVE and TOMBSTONE share one shape; PURGE adds its confirmation. */
1546
+ parseRemovalBody(keyword) {
1547
+ const comments = this.collectLeadingComments();
1548
+ const start = this.currentPos();
1549
+ this.expectKeywordWithSpace(keyword);
1550
+ const target = this.parseTargetRef();
1551
+ let where;
1552
+ if (this.check(TokenType.Where)) {
1553
+ this.expectKeywordWithSpace(TokenType.Where);
1554
+ this.dialect = 'raw';
1555
+ where = this.parseWhereClause();
1076
1556
  }
1077
- this.advance();
1078
- this.expect(TokenType.Colon);
1079
- return this.parseStringOrParameterValue('proposition id');
1557
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1558
+ const expectState = this.check(TokenType.Expect)
1559
+ ? this.parseExpectStateClause()
1560
+ : undefined;
1561
+ return {
1562
+ start,
1563
+ target,
1564
+ where,
1565
+ limit,
1566
+ expectState,
1567
+ leadingComments: comments.length ? comments : undefined
1568
+ };
1080
1569
  }
1081
- parseStringOrParameterValue(context) {
1082
- const tok = this.current();
1570
+ parsePurgeStatement() {
1571
+ const leadingComments = this.collectLeadingComments();
1083
1572
  const start = this.currentPos();
1084
- if (tok.type === TokenType.String) {
1085
- this.advance();
1086
- return {
1087
- kind: 'StringLiteral',
1088
- value: tok.value,
1089
- parsed: this.unescapeString(tok.value, tok),
1090
- range: { start, end: this.currentPos() }
1091
- };
1573
+ this.expectKeywordWithSpace(TokenType.Purge);
1574
+ const target = this.parseTargetRef();
1575
+ let where;
1576
+ if (this.check(TokenType.Where)) {
1577
+ this.expectKeywordWithSpace(TokenType.Where);
1578
+ this.dialect = 'raw';
1579
+ where = this.parseWhereClause();
1092
1580
  }
1093
- if (tok.type === TokenType.Parameter) {
1094
- this.advance();
1095
- return {
1096
- kind: 'ParameterRef',
1097
- name: tok.value,
1098
- range: { start, end: this.currentPos() }
1099
- };
1581
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1582
+ let referencePolicy;
1583
+ if (this.check(TokenType.Reference)) {
1584
+ const reference = this.expect(TokenType.Reference);
1585
+ this.expectSecondWord(TokenType.Policy, reference);
1586
+ referencePolicy = this.parseScalarValue();
1587
+ }
1588
+ // The grammar freezes the confirmation spelling. Physical erasure is
1589
+ // exceptional, so the literal is required and checked here rather than
1590
+ // left for the engine to discover.
1591
+ this.expect(TokenType.Confirm);
1592
+ const confirmTok = this.current();
1593
+ const confirm = this.parseStringLiteral();
1594
+ if (confirm.parsed !== 'PURGE') {
1595
+ this.error(`PURGE must be confirmed with the exact literal "PURGE", got ${confirmTok.value}`, confirmTok);
1100
1596
  }
1101
- this.error(`Expected string or parameter for ${context}`, tok);
1102
1597
  return {
1103
- kind: 'StringLiteral',
1104
- value: '""',
1105
- parsed: '',
1106
- range: { start, end: start }
1598
+ kind: 'PurgeStatement',
1599
+ target,
1600
+ where,
1601
+ limit,
1602
+ referencePolicy,
1603
+ confirm,
1604
+ range: { start, end: this.endPos() },
1605
+ leadingComments: leadingComments.length ? leadingComments : undefined
1107
1606
  };
1108
1607
  }
1109
- isIdKeyToken(tok) {
1110
- return ((tok.type === TokenType.Identifier && tok.value === 'id') ||
1111
- (tok.type === TokenType.String && this.unescapeString(tok.value) === 'id'));
1112
- }
1113
- parseWithMetadata() {
1608
+ parseMergeConcept() {
1609
+ const leadingComments = this.collectLeadingComments();
1114
1610
  const start = this.currentPos();
1115
- const withTok = this.expect(TokenType.With);
1116
- this.expectSecondWord(TokenType.Metadata, withTok);
1117
- this.expect(TokenType.LBrace);
1118
- const entries = this.parseObjectEntries();
1119
- this.expect(TokenType.RBrace);
1611
+ const merge = this.expectKeywordWithSpace(TokenType.Merge);
1612
+ this.expectSecondWord(TokenType.Concept, merge);
1613
+ const source = this.parseTargetRef();
1614
+ this.expect(TokenType.Into);
1615
+ const into = this.parseTargetRef();
1616
+ let where;
1617
+ if (this.check(TokenType.Where)) {
1618
+ this.expectKeywordWithSpace(TokenType.Where);
1619
+ this.dialect = 'raw';
1620
+ where = this.parseWhereClause();
1621
+ }
1622
+ const expectVersion = this.check(TokenType.Expect)
1623
+ ? this.parseExpectVersionClause()
1624
+ : undefined;
1120
1625
  return {
1121
- kind: 'WithMetadata',
1122
- entries,
1123
- range: { start, end: this.currentPos() }
1626
+ kind: 'MergeConceptStatement',
1627
+ source,
1628
+ into,
1629
+ where,
1630
+ expectVersion,
1631
+ range: { start, end: this.endPos() },
1632
+ leadingComments: leadingComments.length ? leadingComments : undefined
1124
1633
  };
1125
1634
  }
1126
- parseExpectVersion() {
1635
+ // ────────────────────────────────────────────────────────────────────
1636
+ // META — DESCRIBE
1637
+ // ────────────────────────────────────────────────────────────────────
1638
+ parseDescribeStatement() {
1639
+ const leadingComments = this.collectLeadingComments();
1640
+ const start = this.currentPos();
1641
+ const describe = this.expectKeywordWithSpace(TokenType.Describe);
1642
+ const tok = this.current();
1643
+ const stmt = (target, extra = {}) => ({
1644
+ kind: 'DescribeStatement',
1645
+ target,
1646
+ ...extra,
1647
+ range: { start, end: this.endPos() },
1648
+ leadingComments: leadingComments.length ? leadingComments : undefined
1649
+ });
1650
+ switch (tok.type) {
1651
+ case TokenType.Primer: {
1652
+ this.expectSecondWord(TokenType.Primer, describe);
1653
+ let mode;
1654
+ if (this.match(TokenType.Mode))
1655
+ mode = this.parseScalarValue();
1656
+ return stmt('PRIMER', { mode });
1657
+ }
1658
+ case TokenType.Protocol:
1659
+ this.expectSecondWord(TokenType.Protocol, describe);
1660
+ return stmt('PROTOCOL');
1661
+ case TokenType.Execution: {
1662
+ const exec = this.expectSecondWord(TokenType.Execution, describe);
1663
+ this.expectSecondWord(TokenType.Context, exec);
1664
+ return stmt('EXECUTION_CONTEXT');
1665
+ }
1666
+ case TokenType.Capabilities:
1667
+ this.expectSecondWord(TokenType.Capabilities, describe);
1668
+ return stmt('CAPABILITIES');
1669
+ case TokenType.Space: {
1670
+ this.expectSecondWord(TokenType.Space, describe);
1671
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined;
1672
+ return stmt('SPACE', { value });
1673
+ }
1674
+ case TokenType.Schema: {
1675
+ const schema = this.expectSecondWord(TokenType.Schema, describe);
1676
+ this.expectSecondWord(TokenType.Environment, schema);
1677
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined;
1678
+ return stmt('SCHEMA_ENVIRONMENT', { asOf });
1679
+ }
1680
+ case TokenType.Package:
1681
+ this.expectSecondWord(TokenType.Package, describe);
1682
+ return stmt('PACKAGE', { value: this.parseScalarValue() });
1683
+ case TokenType.Type:
1684
+ this.expectSecondWord(TokenType.Type, describe);
1685
+ return stmt('TYPE', { value: this.parseScalarValue() });
1686
+ case TokenType.Predicate:
1687
+ this.expectSecondWord(TokenType.Predicate, describe);
1688
+ return stmt('PREDICATE', { value: this.parseScalarValue() });
1689
+ case TokenType.Facet:
1690
+ this.expectSecondWord(TokenType.Facet, describe);
1691
+ return stmt('FACET', { value: this.parseScalarValue() });
1692
+ case TokenType.Structural: {
1693
+ const structural = this.expectSecondWord(TokenType.Structural, describe);
1694
+ this.expectSecondWord(TokenType.Field, structural);
1695
+ return stmt('STRUCTURAL_FIELD', { value: this.parseScalarValue() });
1696
+ }
1697
+ case TokenType.Compatibility: {
1698
+ this.expectSecondWord(TokenType.Compatibility, describe);
1699
+ this.expect(TokenType.From);
1700
+ const from = this.parseScalarValue();
1701
+ this.expect(TokenType.To);
1702
+ const to = this.parseScalarValue();
1703
+ return stmt('COMPATIBILITY', { from, to });
1704
+ }
1705
+ case TokenType.Error:
1706
+ this.expectSecondWord(TokenType.Error, describe);
1707
+ return stmt('ERROR', { value: this.parseScalarValue() });
1708
+ case TokenType.Transaction: {
1709
+ const transaction = this.expectSecondWord(TokenType.Transaction, describe);
1710
+ if (this.check(TokenType.By)) {
1711
+ const by = this.expectSecondWord(TokenType.By, transaction);
1712
+ const idem = this.expectSecondWord(TokenType.Idempotency, by);
1713
+ this.expectSecondWord(TokenType.Key, idem);
1714
+ return stmt('TRANSACTION_BY_IDEMPOTENCY_KEY', {
1715
+ value: this.parseScalarValue()
1716
+ });
1717
+ }
1718
+ return stmt('TRANSACTION', { value: this.parseScalarValue() });
1719
+ }
1720
+ case TokenType.Snapshot: {
1721
+ this.expectSecondWord(TokenType.Snapshot, describe);
1722
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined;
1723
+ return stmt('SNAPSHOT', { asOf });
1724
+ }
1725
+ case TokenType.Capsule:
1726
+ this.expectSecondWord(TokenType.Capsule, describe);
1727
+ return stmt('CAPSULE', { value: this.parseScalarValue() });
1728
+ case TokenType.Epistemic: {
1729
+ const epistemic = this.expectSecondWord(TokenType.Epistemic, describe);
1730
+ this.expectSecondWord(TokenType.Policy, epistemic);
1731
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined;
1732
+ return stmt('EPISTEMIC_POLICY', { value });
1733
+ }
1734
+ case TokenType.Projection: {
1735
+ const projection = this.expectSecondWord(TokenType.Projection, describe);
1736
+ this.expectSecondWord(TokenType.Capability, projection);
1737
+ return stmt('PROJECTION_CAPABILITY');
1738
+ }
1739
+ case TokenType.Trust: {
1740
+ this.expectSecondWord(TokenType.Trust, describe);
1741
+ const value = this.isMetaValueStart() ? this.parseScalarValue() : undefined;
1742
+ return stmt('TRUST', { value });
1743
+ }
1744
+ case TokenType.Access: {
1745
+ this.expectSecondWord(TokenType.Access, describe);
1746
+ const withOptions = this.match(TokenType.With)
1747
+ ? this.parseObjectLiteral()
1748
+ : undefined;
1749
+ return stmt('ACCESS', { with: withOptions });
1750
+ }
1751
+ default:
1752
+ this.error(`Unknown DESCRIBE target '${tok.value}'`, tok);
1753
+ throw new ParseAbort();
1754
+ }
1755
+ }
1756
+ // ────────────────────────────────────────────────────────────────────
1757
+ // META — LIST
1758
+ // ────────────────────────────────────────────────────────────────────
1759
+ parseListStatement() {
1760
+ const leadingComments = this.collectLeadingComments();
1127
1761
  const start = this.currentPos();
1128
- const expectTok = this.expect(TokenType.Expect);
1129
- this.expectSecondWord(TokenType.Version, expectTok);
1130
- const value = this.parseNumberOrParameterValue('EXPECT VERSION');
1762
+ const list = this.expectKeywordWithSpace(TokenType.List);
1763
+ const tok = this.current();
1764
+ let target;
1765
+ let status;
1766
+ switch (tok.type) {
1767
+ case TokenType.Spaces:
1768
+ this.expectSecondWord(TokenType.Spaces, list);
1769
+ target = 'SPACES';
1770
+ break;
1771
+ case TokenType.Schema: {
1772
+ const schema = this.expectSecondWord(TokenType.Schema, list);
1773
+ this.expectSecondWord(TokenType.Packages, schema);
1774
+ target = 'SCHEMA_PACKAGES';
1775
+ if (this.match(TokenType.Status))
1776
+ status = this.parseScalarValue();
1777
+ break;
1778
+ }
1779
+ case TokenType.Types:
1780
+ this.expectSecondWord(TokenType.Types, list);
1781
+ target = 'TYPES';
1782
+ break;
1783
+ case TokenType.Predicates:
1784
+ this.expectSecondWord(TokenType.Predicates, list);
1785
+ target = 'PREDICATES';
1786
+ break;
1787
+ case TokenType.Facets:
1788
+ this.expectSecondWord(TokenType.Facets, list);
1789
+ target = 'FACETS';
1790
+ break;
1791
+ case TokenType.Structural: {
1792
+ const structural = this.expectSecondWord(TokenType.Structural, list);
1793
+ this.expectSecondWord(TokenType.Fields, structural);
1794
+ target = 'STRUCTURAL_FIELDS';
1795
+ break;
1796
+ }
1797
+ case TokenType.Epistemic: {
1798
+ const epistemic = this.expectSecondWord(TokenType.Epistemic, list);
1799
+ this.expectSecondWord(TokenType.Policies, epistemic);
1800
+ target = 'EPISTEMIC_POLICIES';
1801
+ break;
1802
+ }
1803
+ default:
1804
+ this.error(`Unknown LIST target '${tok.value}'`, tok);
1805
+ throw new ParseAbort();
1806
+ }
1807
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1808
+ const cursor = this.check(TokenType.Cursor)
1809
+ ? this.parseCursorClause()
1810
+ : undefined;
1131
1811
  return {
1132
- kind: 'ExpectVersion',
1133
- value,
1134
- range: { start, end: this.currentPos() }
1812
+ kind: 'ListStatement',
1813
+ target,
1814
+ status,
1815
+ limit,
1816
+ cursor,
1817
+ range: { start, end: this.endPos() },
1818
+ leadingComments: leadingComments.length ? leadingComments : undefined
1135
1819
  };
1136
1820
  }
1137
1821
  // ────────────────────────────────────────────────────────────────────
1138
- // ORDER BY, LIMIT, CURSOR
1822
+ // META SEARCH
1139
1823
  // ────────────────────────────────────────────────────────────────────
1140
- parseOrderBy() {
1824
+ parseSearchStatement() {
1825
+ const leadingComments = this.collectLeadingComments();
1141
1826
  const start = this.currentPos();
1142
- const orderTok = this.expect(TokenType.Order);
1143
- this.expectSecondWord(TokenType.By, orderTok);
1144
- const keys = [];
1145
- keys.push(this.parseOrderByKey());
1146
- while (this.match(TokenType.Comma)) {
1147
- keys.push(this.parseOrderByKey());
1827
+ const search = this.expectKeywordWithSpace(TokenType.Search);
1828
+ const kindTok = this.current();
1829
+ let searchKind;
1830
+ switch (kindTok.type) {
1831
+ case TokenType.Concept:
1832
+ searchKind = 'CONCEPT';
1833
+ break;
1834
+ case TokenType.Proposition:
1835
+ searchKind = 'PROPOSITION';
1836
+ break;
1837
+ case TokenType.Assertion:
1838
+ searchKind = 'ASSERTION';
1839
+ break;
1840
+ case TokenType.Evidence:
1841
+ searchKind = 'EVIDENCE';
1842
+ break;
1843
+ case TokenType.Activity:
1844
+ searchKind = 'ACTIVITY';
1845
+ break;
1846
+ case TokenType.Cognition:
1847
+ searchKind = 'COGNITION';
1848
+ break;
1849
+ default:
1850
+ this.error(`Expected CONCEPT, PROPOSITION, ASSERTION, EVIDENCE, ACTIVITY or COGNITION after SEARCH but got '${kindTok.value}'`, kindTok);
1851
+ throw new ParseAbort();
1852
+ }
1853
+ this.expectSecondWord(kindTok.type, search);
1854
+ const term = this.parseScalarValue();
1855
+ let withType;
1856
+ let withPredicate;
1857
+ let mode;
1858
+ let threshold;
1859
+ let asOfSeq;
1860
+ // The grammar fixes this order; each modifier is taken at most once.
1861
+ while (this.check(TokenType.With)) {
1862
+ const withTok = this.expect(TokenType.With);
1863
+ if (this.check(TokenType.Type)) {
1864
+ this.expectSecondWord(TokenType.Type, withTok);
1865
+ this.rejectRepeat(withType, 'WITH TYPE', withTok);
1866
+ withType = this.parseScalarValue();
1867
+ }
1868
+ else if (this.check(TokenType.Predicate)) {
1869
+ this.expectSecondWord(TokenType.Predicate, withTok);
1870
+ this.rejectRepeat(withPredicate, 'WITH PREDICATE', withTok);
1871
+ withPredicate = this.parseScalarValue();
1872
+ }
1873
+ else {
1874
+ this.error(`Expected TYPE or PREDICATE after WITH but got '${this.current().value}'`, this.current());
1875
+ break;
1876
+ }
1148
1877
  }
1149
- const first = keys[0];
1878
+ if (this.match(TokenType.Mode))
1879
+ mode = this.parseScalarValue();
1880
+ if (this.match(TokenType.Threshold))
1881
+ threshold = this.parseScalarValue();
1882
+ if (this.check(TokenType.As)) {
1883
+ const as = this.expect(TokenType.As);
1884
+ const of = this.expectSecondWord(TokenType.Of, as);
1885
+ this.expectSecondWord(TokenType.Seq, of);
1886
+ asOfSeq = this.parseScalarValue();
1887
+ }
1888
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1889
+ const cursor = this.check(TokenType.Cursor)
1890
+ ? this.parseCursorClause()
1891
+ : undefined;
1150
1892
  return {
1151
- kind: 'OrderByClause',
1152
- keys,
1153
- expression: first.expression,
1154
- direction: first.direction,
1155
- range: { start, end: this.currentPos() }
1893
+ kind: 'SearchStatement',
1894
+ searchKind,
1895
+ term,
1896
+ withType,
1897
+ withPredicate,
1898
+ mode,
1899
+ threshold,
1900
+ asOfSeq,
1901
+ limit,
1902
+ cursor,
1903
+ range: { start, end: this.endPos() },
1904
+ leadingComments: leadingComments.length ? leadingComments : undefined
1156
1905
  };
1157
1906
  }
1158
- parseOrderByKey() {
1907
+ // ────────────────────────────────────────────────────────────────────
1908
+ // META — VERIFY / VALIDATE / PREVIEW
1909
+ // ────────────────────────────────────────────────────────────────────
1910
+ parseVerifyStatement() {
1911
+ const leadingComments = this.collectLeadingComments();
1159
1912
  const start = this.currentPos();
1160
- const expression = this.parseExpression();
1161
- let direction = 'ASC';
1162
- if (this.check(TokenType.Asc)) {
1163
- this.advance();
1164
- direction = 'ASC';
1165
- }
1166
- else if (this.check(TokenType.Desc)) {
1167
- this.advance();
1168
- direction = 'DESC';
1913
+ const verify = this.expectKeywordWithSpace(TokenType.Verify);
1914
+ const tok = this.current();
1915
+ let target;
1916
+ switch (tok.type) {
1917
+ case TokenType.Capsule:
1918
+ this.expectSecondWord(TokenType.Capsule, verify);
1919
+ target = 'CAPSULE';
1920
+ break;
1921
+ case TokenType.Schema: {
1922
+ const schema = this.expectSecondWord(TokenType.Schema, verify);
1923
+ this.expectSecondWord(TokenType.Package, schema);
1924
+ target = 'SCHEMA_PACKAGE';
1925
+ break;
1926
+ }
1927
+ case TokenType.Receipt:
1928
+ this.expectSecondWord(TokenType.Receipt, verify);
1929
+ target = 'RECEIPT';
1930
+ break;
1931
+ case TokenType.Blob:
1932
+ this.expectSecondWord(TokenType.Blob, verify);
1933
+ target = 'BLOB';
1934
+ break;
1935
+ case TokenType.Checkpoint:
1936
+ this.expectSecondWord(TokenType.Checkpoint, verify);
1937
+ target = 'CHECKPOINT';
1938
+ break;
1939
+ default:
1940
+ this.error(`Unknown VERIFY target '${tok.value}'`, tok);
1941
+ throw new ParseAbort();
1169
1942
  }
1170
1943
  return {
1171
- kind: 'OrderByKey',
1172
- expression,
1173
- direction,
1174
- range: { start, end: this.currentPos() }
1944
+ kind: 'VerifyStatement',
1945
+ target,
1946
+ value: this.parseScalarValue(),
1947
+ range: { start, end: this.endPos() },
1948
+ leadingComments: leadingComments.length ? leadingComments : undefined
1175
1949
  };
1176
1950
  }
1177
- parseThresholdClause() {
1951
+ parseValidateStatement() {
1952
+ const leadingComments = this.collectLeadingComments();
1178
1953
  const start = this.currentPos();
1179
- this.expectKeywordWithSpace(TokenType.Threshold);
1180
- const value = this.parseNumberOrParameterValue('THRESHOLD');
1954
+ const validate = this.expectKeywordWithSpace(TokenType.Validate);
1955
+ const tok = this.current();
1956
+ let target;
1957
+ switch (tok.type) {
1958
+ case TokenType.Kql:
1959
+ this.expectSecondWord(TokenType.Kql, validate);
1960
+ target = 'KQL';
1961
+ break;
1962
+ case TokenType.Kml:
1963
+ this.expectSecondWord(TokenType.Kml, validate);
1964
+ target = 'KML';
1965
+ break;
1966
+ case TokenType.Capsule:
1967
+ this.expectSecondWord(TokenType.Capsule, validate);
1968
+ target = 'CAPSULE';
1969
+ break;
1970
+ case TokenType.Schema: {
1971
+ const schema = this.expectSecondWord(TokenType.Schema, validate);
1972
+ this.expectSecondWord(TokenType.Package, schema);
1973
+ target = 'SCHEMA_PACKAGE';
1974
+ break;
1975
+ }
1976
+ case TokenType.Import: {
1977
+ const importTok = this.expectSecondWord(TokenType.Import, validate);
1978
+ this.expectSecondWord(TokenType.Plan, importTok);
1979
+ target = 'IMPORT_PLAN';
1980
+ break;
1981
+ }
1982
+ default:
1983
+ this.error(`Unknown VALIDATE target '${tok.value}'`, tok);
1984
+ throw new ParseAbort();
1985
+ }
1986
+ const value = this.parseScalarValue();
1987
+ const options = this.match(TokenType.With)
1988
+ ? this.parseObjectLiteral()
1989
+ : undefined;
1181
1990
  return {
1182
- kind: 'ThresholdClause',
1991
+ kind: 'ValidateStatement',
1992
+ target,
1183
1993
  value,
1184
- range: { start, end: this.currentPos() }
1994
+ options,
1995
+ range: { start, end: this.endPos() },
1996
+ leadingComments: leadingComments.length ? leadingComments : undefined
1185
1997
  };
1186
1998
  }
1187
- parseNumberOrParameterValue(context) {
1188
- const tok = this.current();
1999
+ parsePreviewStatement() {
2000
+ const leadingComments = this.collectLeadingComments();
1189
2001
  const start = this.currentPos();
1190
- let value;
1191
- if (tok.type === TokenType.Number) {
1192
- value = {
1193
- kind: 'NumberLiteral',
1194
- value: Number(tok.value),
1195
- raw: tok.value,
1196
- range: { start, end: start }
2002
+ const preview = this.expectKeywordWithSpace(TokenType.Preview);
2003
+ const tok = this.current();
2004
+ if (tok.type === TokenType.Kml) {
2005
+ this.expectSecondWord(TokenType.Kml, preview);
2006
+ return {
2007
+ kind: 'PreviewStatement',
2008
+ target: 'KML',
2009
+ value: this.parseScalarValue(),
2010
+ range: { start, end: this.endPos() },
2011
+ leadingComments: leadingComments.length ? leadingComments : undefined
1197
2012
  };
1198
- this.advance();
1199
- value.range.end = this.currentPos();
1200
- return value;
1201
2013
  }
1202
- if (tok.type === TokenType.Parameter) {
1203
- value = {
1204
- kind: 'ParameterRef',
1205
- name: tok.value,
1206
- range: { start, end: start }
2014
+ if (tok.type === TokenType.Import) {
2015
+ const importTok = this.expectSecondWord(TokenType.Import, preview);
2016
+ this.expectSecondWord(TokenType.Capsule, importTok);
2017
+ const value = this.parseScalarValue();
2018
+ this.expect(TokenType.Into);
2019
+ const into = this.parseScalarValue();
2020
+ return {
2021
+ kind: 'PreviewStatement',
2022
+ target: 'IMPORT_CAPSULE',
2023
+ value,
2024
+ into,
2025
+ range: { start, end: this.endPos() },
2026
+ leadingComments: leadingComments.length ? leadingComments : undefined
1207
2027
  };
1208
- this.advance();
1209
- value.range.end = this.currentPos();
1210
- return value;
1211
2028
  }
1212
- this.error(`Expected number or parameter after ${context}`, tok);
1213
- return {
1214
- kind: 'NumberLiteral',
1215
- value: 0,
1216
- raw: '0',
1217
- range: { start, end: start }
1218
- };
2029
+ this.error(`Expected KML or IMPORT CAPSULE after PREVIEW but got '${tok.value}'`, tok);
2030
+ throw new ParseAbort();
1219
2031
  }
1220
- parseLimitClause() {
2032
+ // ────────────────────────────────────────────────────────────────────
2033
+ // META — HISTORY / CHANGES / SNAPSHOT
2034
+ // ────────────────────────────────────────────────────────────────────
2035
+ parseHistoryStatement() {
2036
+ const leadingComments = this.collectLeadingComments();
1221
2037
  const start = this.currentPos();
1222
- this.expectKeywordWithSpace(TokenType.Limit);
1223
- const value = this.parseNumberOrParameterValue('LIMIT');
2038
+ const history = this.expectKeywordWithSpace(TokenType.History);
2039
+ const tok = this.current();
2040
+ let target;
2041
+ let value;
2042
+ if (tok.type === TokenType.Element) {
2043
+ this.expectSecondWord(TokenType.Element, history);
2044
+ target = 'ELEMENT';
2045
+ value = this.parseScalarValue();
2046
+ }
2047
+ else if (tok.type === TokenType.Space) {
2048
+ this.expectSecondWord(TokenType.Space, history);
2049
+ target = 'SPACE';
2050
+ }
2051
+ else {
2052
+ this.error(`Expected ELEMENT or SPACE after HISTORY but got '${tok.value}'`, tok);
2053
+ throw new ParseAbort();
2054
+ }
2055
+ let fromSeq;
2056
+ let toSeq;
2057
+ if (this.check(TokenType.From)) {
2058
+ const from = this.expect(TokenType.From);
2059
+ this.expectSecondWord(TokenType.Seq, from);
2060
+ fromSeq = this.parseScalarValue();
2061
+ }
2062
+ if (this.check(TokenType.To)) {
2063
+ const to = this.expect(TokenType.To);
2064
+ this.expectSecondWord(TokenType.Seq, to);
2065
+ toSeq = this.parseScalarValue();
2066
+ }
2067
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
2068
+ const cursor = this.check(TokenType.Cursor)
2069
+ ? this.parseCursorClause()
2070
+ : undefined;
1224
2071
  return {
1225
- kind: 'LimitClause',
2072
+ kind: 'HistoryStatement',
2073
+ target,
1226
2074
  value,
1227
- range: { start, end: this.currentPos() }
2075
+ fromSeq,
2076
+ toSeq,
2077
+ limit,
2078
+ cursor,
2079
+ range: { start, end: this.endPos() },
2080
+ leadingComments: leadingComments.length ? leadingComments : undefined
1228
2081
  };
1229
2082
  }
1230
- parseCursorClause() {
2083
+ parseChangesStatement() {
2084
+ const leadingComments = this.collectLeadingComments();
1231
2085
  const start = this.currentPos();
1232
- this.expectKeywordWithSpace(TokenType.Cursor);
1233
- const tok = this.current();
1234
- let value;
1235
- if (tok.type === TokenType.String) {
1236
- value = {
1237
- kind: 'StringLiteral',
1238
- value: tok.value,
1239
- parsed: this.unescapeString(tok.value, tok),
1240
- range: { start: this.currentPos(), end: this.currentPos() }
1241
- };
1242
- this.advance();
1243
- value.range.end = this.currentPos();
2086
+ const changes = this.expectKeywordWithSpace(TokenType.Changes);
2087
+ let mode;
2088
+ if (this.check(TokenType.Since)) {
2089
+ this.expectSecondWord(TokenType.Since, changes);
2090
+ mode = 'SINCE';
1244
2091
  }
1245
- else if (tok.type === TokenType.Parameter) {
1246
- value = {
1247
- kind: 'ParameterRef',
1248
- name: tok.value,
1249
- range: { start: this.currentPos(), end: this.currentPos() }
1250
- };
1251
- this.advance();
1252
- value.range.end = this.currentPos();
2092
+ else if (this.check(TokenType.After)) {
2093
+ const after = this.expectSecondWord(TokenType.After, changes);
2094
+ this.expectSecondWord(TokenType.Seq, after);
2095
+ mode = 'AFTER_SEQ';
1253
2096
  }
1254
2097
  else {
1255
- this.error(`Expected string or parameter after CURSOR`, tok);
1256
- value = {
1257
- kind: 'StringLiteral',
1258
- value: '""',
1259
- parsed: '',
1260
- range: { start: this.currentPos(), end: this.currentPos() }
1261
- };
2098
+ this.error(`Expected SINCE or AFTER SEQ after CHANGES but got '${this.current().value}'`, this.current());
2099
+ throw new ParseAbort();
1262
2100
  }
2101
+ const value = this.parseScalarValue();
2102
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1263
2103
  return {
1264
- kind: 'CursorClause',
2104
+ kind: 'ChangesStatement',
2105
+ mode,
1265
2106
  value,
1266
- range: { start, end: this.currentPos() }
2107
+ limit,
2108
+ range: { start, end: this.endPos() },
2109
+ leadingComments: leadingComments.length ? leadingComments : undefined
2110
+ };
2111
+ }
2112
+ parseSnapshotStatement() {
2113
+ const leadingComments = this.collectLeadingComments();
2114
+ const start = this.currentPos();
2115
+ this.expect(TokenType.Snapshot);
2116
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined;
2117
+ return {
2118
+ kind: 'SnapshotStatement',
2119
+ asOf,
2120
+ range: { start, end: this.endPos() },
2121
+ leadingComments: leadingComments.length ? leadingComments : undefined
2122
+ };
2123
+ }
2124
+ // ────────────────────────────────────────────────────────────────────
2125
+ // META — EXPORT CAPSULE
2126
+ // ────────────────────────────────────────────────────────────────────
2127
+ parseExportCapsuleStatement() {
2128
+ const leadingComments = this.collectLeadingComments();
2129
+ const start = this.currentPos();
2130
+ const exportTok = this.expectKeywordWithSpace(TokenType.Export);
2131
+ this.expectSecondWord(TokenType.Capsule, exportTok);
2132
+ const target = this.parseTargetRef();
2133
+ this.expectKeywordWithSpace(TokenType.Where);
2134
+ // A capsule carries records, not interpretations: BELIEF is excluded.
2135
+ this.dialect = 'raw';
2136
+ const where = this.parseWhereClause();
2137
+ const options = this.match(TokenType.With)
2138
+ ? this.parseObjectLiteral()
2139
+ : undefined;
2140
+ const asOf = this.check(TokenType.As) ? this.parseAsOfClause() : undefined;
2141
+ return {
2142
+ kind: 'ExportCapsuleStatement',
2143
+ target,
2144
+ where,
2145
+ options,
2146
+ asOf,
2147
+ range: { start, end: this.endPos() },
2148
+ leadingComments: leadingComments.length ? leadingComments : undefined
1267
2149
  };
1268
2150
  }
1269
2151
  // ────────────────────────────────────────────────────────────────────
1270
- // Expressions (for FILTER and FIND projections)
2152
+ // Expressions
1271
2153
  // ────────────────────────────────────────────────────────────────────
1272
2154
  parseExpression() {
1273
2155
  return this.parseOrExpression();
@@ -1283,317 +2165,540 @@ class Parser {
1283
2165
  operator: '||',
1284
2166
  left,
1285
2167
  right,
1286
- range: { start, end: right.range.end }
2168
+ range: { start, end: this.endPos() }
1287
2169
  };
1288
2170
  }
1289
2171
  return left;
1290
2172
  }
1291
2173
  parseAndExpression() {
1292
- let left = this.parseComparisonExpression();
2174
+ let left = this.parseEqualityExpression();
1293
2175
  while (this.check(TokenType.And)) {
1294
2176
  const start = left.range.start;
1295
2177
  this.advance();
1296
- const right = this.parseComparisonExpression();
2178
+ const right = this.parseEqualityExpression();
1297
2179
  left = {
1298
2180
  kind: 'BinaryExpression',
1299
2181
  operator: '&&',
1300
2182
  left,
1301
2183
  right,
1302
- range: { start, end: right.range.end }
2184
+ range: { start, end: this.endPos() }
1303
2185
  };
1304
2186
  }
1305
2187
  return left;
1306
2188
  }
1307
- parseComparisonExpression() {
1308
- let left = this.parseUnaryExpression();
1309
- const compOps = [
1310
- TokenType.Eq,
1311
- TokenType.NotEq,
1312
- TokenType.Lt,
1313
- TokenType.Gt,
1314
- TokenType.LtEq,
1315
- TokenType.GtEq
1316
- ];
1317
- if (compOps.includes(this.current().type)) {
2189
+ parseEqualityExpression() {
2190
+ let left = this.parseRelationalExpression();
2191
+ while (this.check(TokenType.Eq) || this.check(TokenType.NotEq)) {
1318
2192
  const start = left.range.start;
1319
- const op = this.current().value;
1320
- this.advance();
1321
- const right = this.parseUnaryExpression();
2193
+ const op = this.advance().type;
2194
+ const right = this.parseRelationalExpression();
1322
2195
  left = {
1323
2196
  kind: 'BinaryExpression',
1324
- operator: op,
2197
+ operator: op === TokenType.Eq ? '==' : '!=',
1325
2198
  left,
1326
2199
  right,
1327
- range: { start, end: right.range.end }
2200
+ range: { start, end: this.endPos() }
1328
2201
  };
1329
2202
  }
1330
- return left;
2203
+ return left;
2204
+ }
2205
+ /**
2206
+ * `relational_expression` takes at most one comparison.
2207
+ *
2208
+ * `a < b < c` is not chained comparison in KIP, it is a grammar error, and
2209
+ * accepting it here would give the parser a meaning the reference grammar
2210
+ * does not define.
2211
+ */
2212
+ parseRelationalExpression() {
2213
+ const left = this.parseUnaryExpression();
2214
+ const tok = this.current();
2215
+ let operator;
2216
+ switch (tok.type) {
2217
+ case TokenType.Lt:
2218
+ operator = '<';
2219
+ break;
2220
+ case TokenType.Gt:
2221
+ operator = '>';
2222
+ break;
2223
+ case TokenType.LtEq:
2224
+ operator = '<=';
2225
+ break;
2226
+ case TokenType.GtEq:
2227
+ operator = '>=';
2228
+ break;
2229
+ default:
2230
+ return left;
2231
+ }
2232
+ this.advance();
2233
+ const right = this.parseUnaryExpression();
2234
+ const result = {
2235
+ kind: 'BinaryExpression',
2236
+ operator,
2237
+ left,
2238
+ right,
2239
+ range: { start: left.range.start, end: this.endPos() }
2240
+ };
2241
+ const next = this.current();
2242
+ if (next.type === TokenType.Lt ||
2243
+ next.type === TokenType.Gt ||
2244
+ next.type === TokenType.LtEq ||
2245
+ next.type === TokenType.GtEq) {
2246
+ this.error(`Chained comparison '${next.value}' is not allowed; use && between comparisons`, next);
2247
+ }
2248
+ return result;
1331
2249
  }
1332
2250
  parseUnaryExpression() {
1333
- if (this.check(TokenType.Bang)) {
2251
+ const tok = this.current();
2252
+ if (tok.type === TokenType.Bang || tok.type === TokenType.Minus) {
1334
2253
  const start = this.currentPos();
1335
2254
  this.advance();
1336
- const operand = this.parseUnaryExpression();
2255
+ const operand = this.parsePrimaryExpression();
1337
2256
  return {
1338
2257
  kind: 'UnaryExpression',
1339
- operator: '!',
2258
+ operator: tok.type === TokenType.Bang ? '!' : '-',
1340
2259
  operand,
1341
- range: { start, end: operand.range.end }
2260
+ range: { start, end: this.endPos() }
1342
2261
  };
1343
2262
  }
1344
2263
  return this.parsePrimaryExpression();
1345
2264
  }
1346
2265
  parsePrimaryExpression() {
1347
2266
  const tok = this.current();
1348
- const start = this.currentPos();
1349
- // Function call: NAME(...)
1350
- if (this.isFunctionToken(tok.type)) {
1351
- return this.parseFunctionCall();
2267
+ switch (tok.type) {
2268
+ case TokenType.Variable:
2269
+ return this.parseFieldAccessOrVariable();
2270
+ case TokenType.Parameter:
2271
+ return this.parseParameterRef();
2272
+ case TokenType.String:
2273
+ case TokenType.Number:
2274
+ case TokenType.Boolean:
2275
+ case TokenType.Null:
2276
+ return this.parseLiteral();
2277
+ case TokenType.LBracket:
2278
+ return this.parseArrayLiteral();
2279
+ case TokenType.LBrace:
2280
+ return this.parseObjectLiteral();
2281
+ case TokenType.LParen: {
2282
+ this.advance();
2283
+ const inner = this.parseExpression();
2284
+ this.expect(TokenType.RParen);
2285
+ return inner;
2286
+ }
2287
+ default:
2288
+ // `function_call` is an open `identifier "("`, and KIP 2.0 keywords
2289
+ // are contextual, so any identifier-like token may name a function.
2290
+ if (isIdentifierLike(tok.type) && this.peekPast(1)?.type === TokenType.LParen) {
2291
+ return this.parseCallExpression();
2292
+ }
2293
+ this.error(`Unexpected token '${tok.value}' in expression`, tok);
2294
+ this.advance();
2295
+ return {
2296
+ kind: 'NullLiteral',
2297
+ range: { start: this.currentPos(), end: this.endPos() }
2298
+ };
1352
2299
  }
1353
- // Variable (may have dot access)
1354
- if (tok.type === TokenType.Variable) {
1355
- const name = tok.value;
1356
- this.advance();
1357
- let expr = {
1358
- kind: 'VariableRef',
1359
- name,
1360
- range: { start, end: this.currentPos() }
2300
+ }
2301
+ /** `COUNT(DISTINCT ?x)` where the name is an aggregate, else a plain call. */
2302
+ parseCallExpression() {
2303
+ const start = this.currentPos();
2304
+ const nameTok = this.advance();
2305
+ const name = nameTok.value;
2306
+ this.expect(TokenType.LParen);
2307
+ if (isAggregate(name)) {
2308
+ const distinct = this.match(TokenType.Distinct);
2309
+ const argument = this.parseExpression();
2310
+ this.expect(TokenType.RParen);
2311
+ return {
2312
+ kind: 'AggregateExpr',
2313
+ name: name.toUpperCase(),
2314
+ distinct,
2315
+ argument,
2316
+ range: { start, end: this.endPos() }
1361
2317
  };
1362
- // Dot access chain. A dot path is written with no whitespace anywhere
1363
- // inside it: `?x.name` is a path, but `?x. name` is a path followed by
1364
- // stray input, and reading them alike would let `ORDER BY ?x.name. ASC`
1365
- // silently sort by a field named `ASC` with no direction.
1366
- let prevEnd = tok.offset + tok.value.length;
1367
- while (this.check(TokenType.Dot)) {
1368
- const dotTok = this.current();
1369
- if (dotTok.offset !== prevEnd) {
1370
- this.error(`Unexpected whitespace before '.' in a dot path`, dotTok);
1371
- break;
1372
- }
2318
+ }
2319
+ const args = [];
2320
+ if (!this.check(TokenType.RParen)) {
2321
+ do {
2322
+ args.push(this.parseExpression());
2323
+ } while (this.match(TokenType.Comma));
2324
+ }
2325
+ this.expect(TokenType.RParen);
2326
+ return {
2327
+ kind: 'FunctionCallExpr',
2328
+ name,
2329
+ args,
2330
+ range: { start, end: this.endPos() }
2331
+ };
2332
+ }
2333
+ /**
2334
+ * `field_access = variable, { field_step }`.
2335
+ *
2336
+ * A dot path carries no whitespace: `?x . name` is three tokens to a
2337
+ * conformant engine, not one path, so the gap is checked here.
2338
+ */
2339
+ parseFieldAccessOrVariable() {
2340
+ const base = this.parseVariableRef();
2341
+ if (!this.isTightFieldStepStart())
2342
+ return base;
2343
+ const steps = [];
2344
+ while (this.isTightFieldStepStart()) {
2345
+ const start = this.currentPos();
2346
+ if (this.check(TokenType.Dot)) {
1373
2347
  this.advance();
1374
- const propTok = this.current();
1375
- if (propTok.offset !== dotTok.offset + 1) {
1376
- this.error(`Expected property name after '.'`, propTok);
1377
- break;
1378
- }
1379
- if (propTok.type === TokenType.Identifier ||
1380
- this.isNonAmbiguousKeyword(propTok.type)) {
1381
- const prop = propTok.value;
1382
- prevEnd = propTok.offset + propTok.value.length;
1383
- this.advance();
1384
- expr = {
1385
- kind: 'DotExpression',
1386
- object: expr,
1387
- property: prop,
1388
- range: { start, end: this.currentPos() }
1389
- };
1390
- }
1391
- else {
1392
- this.error(`Expected property name after '.'`, propTok);
2348
+ const nameTok = this.current();
2349
+ if (!isIdentifierLike(nameTok.type)) {
2350
+ this.error(`Expected a field name after '.' but got '${nameTok.value}'`, nameTok);
1393
2351
  break;
1394
2352
  }
2353
+ this.advance();
2354
+ steps.push({
2355
+ kind: 'DotStep',
2356
+ name: nameTok.value,
2357
+ range: { start, end: this.endPos() }
2358
+ });
2359
+ }
2360
+ else {
2361
+ this.advance(); // [
2362
+ const key = this.parseStringLiteral();
2363
+ this.expect(TokenType.RBracket);
2364
+ steps.push({
2365
+ kind: 'IndexStep',
2366
+ key,
2367
+ range: { start, end: this.endPos() }
2368
+ });
1395
2369
  }
1396
- return expr;
1397
- }
1398
- // Parameter ref
1399
- if (tok.type === TokenType.Parameter) {
1400
- this.advance();
1401
- return {
1402
- kind: 'ParameterRef',
1403
- name: tok.value,
1404
- range: { start, end: this.currentPos() }
1405
- };
1406
2370
  }
1407
- // String literal
1408
- if (tok.type === TokenType.String) {
1409
- this.advance();
1410
- return {
1411
- kind: 'StringLiteral',
1412
- value: tok.value,
1413
- parsed: this.unescapeString(tok.value, tok),
1414
- range: { start, end: this.currentPos() }
1415
- };
2371
+ return {
2372
+ kind: 'FieldAccess',
2373
+ base,
2374
+ steps,
2375
+ range: { start: base.range.start, end: this.endPos() }
2376
+ };
2377
+ }
2378
+ /** True when a `.`/`[` follows with no gap, i.e. continues the path. */
2379
+ isTightFieldStepStart() {
2380
+ const tok = this.current();
2381
+ if (tok.type !== TokenType.Dot && tok.type !== TokenType.LBracket) {
2382
+ return false;
1416
2383
  }
1417
- // Number literal
1418
- if (tok.type === TokenType.Number) {
1419
- this.advance();
2384
+ const prev = this.tokens[this.pos - 1];
2385
+ if (!prev)
2386
+ return false;
2387
+ return prev.offset + prev.value.length === tok.offset;
2388
+ }
2389
+ // ────────────────────────────────────────────────────────────────────
2390
+ // Values, objects, arrays
2391
+ // ────────────────────────────────────────────────────────────────────
2392
+ parseVariableRef() {
2393
+ const tok = this.current();
2394
+ if (tok.type !== TokenType.Variable) {
2395
+ this.error(`Expected a variable (e.g. ?name) but got '${tok.value}'`, tok);
1420
2396
  return {
1421
- kind: 'NumberLiteral',
1422
- value: Number(tok.value),
1423
- raw: tok.value,
1424
- range: { start, end: this.currentPos() }
2397
+ kind: 'VariableRef',
2398
+ name: '?unknown',
2399
+ range: { start: this.currentPos(), end: this.endPos() }
1425
2400
  };
1426
2401
  }
1427
- // Boolean
1428
- if (tok.type === TokenType.Boolean) {
1429
- this.advance();
2402
+ const start = this.currentPos();
2403
+ this.advance();
2404
+ return {
2405
+ kind: 'VariableRef',
2406
+ name: tok.value,
2407
+ range: { start, end: this.endPos() }
2408
+ };
2409
+ }
2410
+ parseParameterRef() {
2411
+ const tok = this.current();
2412
+ const start = this.currentPos();
2413
+ this.advance();
2414
+ return {
2415
+ kind: 'ParameterRef',
2416
+ name: tok.value,
2417
+ range: { start, end: this.endPos() }
2418
+ };
2419
+ }
2420
+ parseStringLiteral() {
2421
+ const tok = this.current();
2422
+ if (tok.type !== TokenType.String) {
2423
+ this.error(`Expected a quoted string but got '${tok.value}'`, tok);
1430
2424
  return {
1431
- kind: 'BooleanLiteral',
1432
- value: tok.value === 'true',
1433
- range: { start, end: this.currentPos() }
2425
+ kind: 'StringLiteral',
2426
+ value: '""',
2427
+ parsed: '',
2428
+ range: { start: this.currentPos(), end: this.endPos() }
1434
2429
  };
1435
2430
  }
1436
- // Null
1437
- if (tok.type === TokenType.Null) {
1438
- this.advance();
1439
- return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
2431
+ const start = this.currentPos();
2432
+ this.advance();
2433
+ return {
2434
+ kind: 'StringLiteral',
2435
+ value: tok.value,
2436
+ parsed: this.unescapeString(tok.value, tok),
2437
+ range: { start, end: this.endPos() }
2438
+ };
2439
+ }
2440
+ parseLiteral() {
2441
+ const tok = this.current();
2442
+ const start = this.currentPos();
2443
+ switch (tok.type) {
2444
+ case TokenType.String:
2445
+ return this.parseStringLiteral();
2446
+ case TokenType.Number: {
2447
+ this.advance();
2448
+ const value = Number(tok.value);
2449
+ if (!Number.isFinite(value)) {
2450
+ this.error(`Only finite numbers are valid KIP literals, got '${tok.value}'`, tok);
2451
+ }
2452
+ return {
2453
+ kind: 'NumberLiteral',
2454
+ value,
2455
+ raw: tok.value,
2456
+ range: { start, end: this.endPos() }
2457
+ };
2458
+ }
2459
+ case TokenType.Boolean:
2460
+ this.advance();
2461
+ return {
2462
+ kind: 'BooleanLiteral',
2463
+ value: tok.value === 'true',
2464
+ range: { start, end: this.endPos() }
2465
+ };
2466
+ case TokenType.Null:
2467
+ this.advance();
2468
+ return { kind: 'NullLiteral', range: { start, end: this.endPos() } };
2469
+ default:
2470
+ this.error(`Expected a literal but got '${tok.value}'`, tok);
2471
+ this.advance();
2472
+ return { kind: 'NullLiteral', range: { start, end: this.endPos() } };
1440
2473
  }
1441
- // Array
1442
- if (tok.type === TokenType.LBracket) {
1443
- return this.parseArrayLiteral();
2474
+ }
2475
+ /** `scalar_value` / `meta_value` = `parameter | literal` */
2476
+ parseScalarValue() {
2477
+ const tok = this.current();
2478
+ if (tok.type === TokenType.Parameter) {
2479
+ return this.parseParameterRef();
1444
2480
  }
1445
- // Object
1446
- if (tok.type === TokenType.LBrace) {
1447
- return this.parseObjectLiteral();
2481
+ if (tok.type === TokenType.String ||
2482
+ tok.type === TokenType.Number ||
2483
+ tok.type === TokenType.Boolean ||
2484
+ tok.type === TokenType.Null) {
2485
+ return this.parseLiteral();
1448
2486
  }
1449
- // Parenthesized expression
1450
- if (tok.type === TokenType.LParen) {
1451
- this.advance();
1452
- const expr = this.parseExpression();
1453
- this.expect(TokenType.RParen);
1454
- return expr;
2487
+ this.error(`Expected a literal or :parameter but got '${tok.value}'`, tok);
2488
+ this.advance();
2489
+ return {
2490
+ kind: 'NullLiteral',
2491
+ range: { start: this.currentPos(), end: this.endPos() }
2492
+ };
2493
+ }
2494
+ /** True when the next token could begin a `meta_value`. */
2495
+ isMetaValueStart() {
2496
+ const t = this.current().type;
2497
+ return (t === TokenType.Parameter ||
2498
+ t === TokenType.String ||
2499
+ t === TokenType.Number ||
2500
+ t === TokenType.Boolean ||
2501
+ t === TokenType.Null);
2502
+ }
2503
+ /** `schema_symbol = string_literal | parameter` */
2504
+ parseSchemaSymbol() {
2505
+ const tok = this.current();
2506
+ if (tok.type === TokenType.Parameter) {
2507
+ return this.parseParameterRef();
1455
2508
  }
1456
- // System identifier as literal
1457
- if (tok.type === TokenType.SystemIdent) {
1458
- this.error(`Unquoted value '${tok.value}': KIP values are JSON values, so write "${tok.value}"`, tok);
1459
- this.advance();
1460
- return {
1461
- kind: 'StringLiteral',
1462
- value: `"${tok.value}"`,
1463
- parsed: tok.value,
1464
- range: { start, end: this.currentPos() }
1465
- };
2509
+ if (tok.type === TokenType.String) {
2510
+ return this.parseStringLiteral();
1466
2511
  }
1467
- // A bare word is not a KIP value only object *keys* may go unquoted, and
1468
- // those never reach here. Recover as a string so the tree stays usable in
1469
- // an editor, and report it: `lower` sees only the tree, so it is the
1470
- // caller's error-diagnostic check that keeps this reading off the wire.
1471
- if (tok.type === TokenType.Identifier) {
1472
- this.error(`Unquoted value '${tok.value}': KIP values are JSON values, so write "${tok.value}"`, tok);
1473
- this.advance();
2512
+ this.error(`Expected a schema symbol (quoted name or :parameter) but got '${tok.value}'`, tok);
2513
+ this.advance();
2514
+ return {
2515
+ kind: 'StringLiteral',
2516
+ value: '""',
2517
+ parsed: '',
2518
+ range: { start: this.currentPos(), end: this.endPos() }
2519
+ };
2520
+ }
2521
+ /** `target_ref = variable | parameter | string_literal` */
2522
+ parseTargetRef() {
2523
+ const tok = this.current();
2524
+ if (tok.type === TokenType.Variable)
2525
+ return this.parseVariableRef();
2526
+ if (tok.type === TokenType.Parameter)
2527
+ return this.parseParameterRef();
2528
+ if (tok.type === TokenType.String)
2529
+ return this.parseStringLiteral();
2530
+ this.error(`Expected a target (?variable, :parameter or quoted id) but got '${tok.value}'`, tok);
2531
+ this.advance();
2532
+ return {
2533
+ kind: 'StringLiteral',
2534
+ value: '""',
2535
+ parsed: '',
2536
+ range: { start: this.currentPos(), end: this.endPos() }
2537
+ };
2538
+ }
2539
+ expectHandle() {
2540
+ const tok = this.current();
2541
+ if (tok.type !== TokenType.Variable) {
2542
+ this.error(`Expected a local handle (e.g. ?e) but got '${tok.value}'`, tok);
1474
2543
  return {
1475
- kind: 'StringLiteral',
1476
- value: `"${tok.value}"`,
1477
- parsed: tok.value,
1478
- range: { start, end: this.currentPos() }
2544
+ kind: 'VariableRef',
2545
+ name: '?unknown',
2546
+ range: { start: this.currentPos(), end: this.endPos() }
1479
2547
  };
1480
2548
  }
1481
- this.error(`Unexpected token '${tok.value}' in expression`, tok);
1482
- this.advance();
1483
- return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
2549
+ return this.parseVariableRef();
1484
2550
  }
1485
- parseFunctionCall() {
1486
- const start = this.currentPos();
1487
- const name = this.current().value;
1488
- this.advance();
1489
- this.expect(TokenType.LParen);
1490
- const args = [];
1491
- if (!this.check(TokenType.RParen)) {
1492
- // Handle DISTINCT keyword inside COUNT
1493
- if (this.current().type === TokenType.Distinct) {
1494
- const dStart = this.currentPos();
2551
+ /** `mutation_value` — everything a KML assignment may hold. */
2552
+ parseMutationValue() {
2553
+ const tok = this.current();
2554
+ switch (tok.type) {
2555
+ case TokenType.Variable:
2556
+ return this.parseFieldAccessOrVariable();
2557
+ case TokenType.Parameter:
2558
+ return this.parseParameterRef();
2559
+ case TokenType.LBracket:
2560
+ return this.parseArrayLiteral();
2561
+ case TokenType.LBrace:
2562
+ return this.parseObjectLiteral();
2563
+ case TokenType.String:
2564
+ case TokenType.Number:
2565
+ case TokenType.Boolean:
2566
+ case TokenType.Null:
2567
+ return this.parseLiteral();
2568
+ default:
2569
+ if (isIdentifierLike(tok.type) && this.peekPast(1)?.type === TokenType.LParen) {
2570
+ return this.parseCallExpression();
2571
+ }
2572
+ this.error(`Unexpected token '${tok.value}' in assignment value`, tok);
1495
2573
  this.advance();
1496
- const innerArg = this.parseExpression();
1497
- args.push({
1498
- kind: 'FunctionCallExpr',
1499
- name: 'DISTINCT',
1500
- args: [innerArg],
1501
- range: { start: dStart, end: this.currentPos() }
1502
- });
1503
- }
1504
- else {
1505
- args.push(this.parseExpression());
1506
- }
1507
- while (this.match(TokenType.Comma)) {
1508
- args.push(this.parseExpression());
1509
- }
2574
+ return {
2575
+ kind: 'NullLiteral',
2576
+ range: { start: this.currentPos(), end: this.endPos() }
2577
+ };
1510
2578
  }
1511
- this.expect(TokenType.RParen);
2579
+ }
2580
+ /** `assignment_object = "{" [assignment_member {"," assignment_member}] "}"` */
2581
+ parseAssignmentObject() {
2582
+ const start = this.currentPos();
2583
+ this.expect(TokenType.LBrace);
2584
+ const seen = { trailingComma: false };
2585
+ const entries = this.parseEntries(seen, () => this.parseMutationValue());
2586
+ this.expect(TokenType.RBrace);
1512
2587
  return {
1513
- kind: 'FunctionCallExpr',
1514
- name,
1515
- args,
1516
- range: { start, end: this.currentPos() }
2588
+ kind: 'ObjectLiteral',
2589
+ entries,
2590
+ trailingComma: seen.trailingComma || undefined,
2591
+ range: { start, end: this.endPos() }
2592
+ };
2593
+ }
2594
+ /** `object_pattern` — `{...}` in matching position. */
2595
+ parseObjectPattern() {
2596
+ const start = this.currentPos();
2597
+ this.expect(TokenType.LBrace);
2598
+ const seen = { trailingComma: false };
2599
+ const members = this.parseEntries(seen, () => this.parsePatternValue());
2600
+ this.expect(TokenType.RBrace);
2601
+ return {
2602
+ kind: 'ObjectPattern',
2603
+ members,
2604
+ trailingComma: seen.trailingComma || undefined,
2605
+ range: { start, end: this.endPos() }
1517
2606
  };
1518
2607
  }
2608
+ parsePatternValue() {
2609
+ const tok = this.current();
2610
+ switch (tok.type) {
2611
+ case TokenType.Variable:
2612
+ return this.parseVariableRef();
2613
+ case TokenType.Parameter:
2614
+ return this.parseParameterRef();
2615
+ case TokenType.LBracket:
2616
+ return this.parseArrayPattern();
2617
+ case TokenType.LBrace:
2618
+ return this.parseObjectPattern();
2619
+ case TokenType.LParen:
2620
+ return this.parsePropositionTuple();
2621
+ case TokenType.String:
2622
+ case TokenType.Number:
2623
+ case TokenType.Boolean:
2624
+ case TokenType.Null:
2625
+ return this.parseLiteral();
2626
+ default:
2627
+ this.error(`Unexpected token '${tok.value}' in match pattern`, tok);
2628
+ this.advance();
2629
+ return {
2630
+ kind: 'NullLiteral',
2631
+ range: { start: this.currentPos(), end: this.endPos() }
2632
+ };
2633
+ }
2634
+ }
2635
+ parseArrayPattern() {
2636
+ return this.parseArrayWith(() => this.parsePatternValue());
2637
+ }
1519
2638
  parseArrayLiteral() {
2639
+ return this.parseArrayWith(() => this.parseExpression());
2640
+ }
2641
+ parseArrayWith(parseElement) {
1520
2642
  const start = this.currentPos();
1521
2643
  this.expect(TokenType.LBracket);
1522
2644
  const elements = [];
1523
2645
  let trailingComma = false;
1524
- this.skipComments();
1525
2646
  if (!this.check(TokenType.RBracket)) {
1526
- elements.push(this.parseExpression());
1527
- while (this.match(TokenType.Comma)) {
1528
- this.skipComments();
2647
+ do {
1529
2648
  if (this.check(TokenType.RBracket)) {
1530
2649
  trailingComma = true;
1531
2650
  break;
1532
2651
  }
1533
- elements.push(this.parseExpression());
1534
- }
2652
+ elements.push(parseElement());
2653
+ } while (this.match(TokenType.Comma));
1535
2654
  }
1536
- this.skipComments();
1537
2655
  this.expect(TokenType.RBracket);
1538
2656
  return {
1539
2657
  kind: 'ArrayLiteral',
1540
2658
  elements,
1541
- trailingComma,
1542
- range: { start, end: this.currentPos() }
2659
+ trailingComma: trailingComma || undefined,
2660
+ range: { start, end: this.endPos() }
1543
2661
  };
1544
2662
  }
1545
2663
  parseObjectLiteral() {
1546
2664
  const start = this.currentPos();
1547
2665
  this.expect(TokenType.LBrace);
1548
2666
  const seen = { trailingComma: false };
1549
- const entries = this.parseObjectEntries(seen);
2667
+ const entries = this.parseEntries(seen, () => this.parseExpression());
1550
2668
  this.expect(TokenType.RBrace);
1551
2669
  return {
1552
2670
  kind: 'ObjectLiteral',
1553
2671
  entries,
1554
- trailingComma: seen.trailingComma,
1555
- range: { start, end: this.currentPos() }
2672
+ trailingComma: seen.trailingComma || undefined,
2673
+ range: { start, end: this.endPos() }
1556
2674
  };
1557
2675
  }
1558
- parseObjectEntries(seen) {
2676
+ parseEntries(seen, parseValue) {
1559
2677
  const entries = [];
1560
- this.skipComments();
1561
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1562
- const before = this.pos;
2678
+ if (this.check(TokenType.RBrace))
2679
+ return entries;
2680
+ do {
1563
2681
  this.skipComments();
1564
- if (this.check(TokenType.RBrace))
2682
+ if (this.check(TokenType.RBrace)) {
2683
+ seen.trailingComma = entries.length > 0;
1565
2684
  break;
1566
- const entryStart = this.currentPos();
2685
+ }
2686
+ const start = this.currentPos();
1567
2687
  const { key, isQuoted } = this.expectKeyWithQuoting();
1568
2688
  this.expectObjectColon(key);
1569
- const value = this.parseExpression();
2689
+ const value = parseValue();
1570
2690
  entries.push({
1571
2691
  kind: 'ObjectEntry',
1572
2692
  key,
1573
2693
  isQuoted,
1574
2694
  value,
1575
- range: { start: entryStart, end: this.currentPos() }
2695
+ range: { start, end: this.endPos() }
1576
2696
  });
1577
- this.skipComments();
1578
- if (this.check(TokenType.RBrace))
1579
- break;
1580
- if (this.match(TokenType.Comma)) {
1581
- this.skipComments();
1582
- if (seen && this.check(TokenType.RBrace))
1583
- seen.trailingComma = true;
1584
- continue;
1585
- }
1586
- this.error(`Expected ',' or '}' after object entry`, this.current());
1587
- // A sub-parser that rejects its first token reports and returns
1588
- // without consuming it, so a loop keyed on that token would spin
1589
- // forever building diagnostics. Stop as soon as nothing moved.
1590
- if (this.pos === before)
1591
- break;
1592
- }
2697
+ } while (this.match(TokenType.Comma));
1593
2698
  return entries;
1594
2699
  }
1595
2700
  // ────────────────────────────────────────────────────────────────────
1596
- // Helpers
2701
+ // Token helpers
1597
2702
  // ────────────────────────────────────────────────────────────────────
1598
2703
  current() {
1599
2704
  return (this.tokens[this.pos] ?? {
@@ -1608,6 +2713,24 @@ class Parser {
1608
2713
  const tok = this.current();
1609
2714
  return { line: tok.line, column: tok.column };
1610
2715
  }
2716
+ /**
2717
+ * The end of the most recently consumed token — where a node actually ends.
2718
+ *
2719
+ * `currentPos()` points at the *next* token, so using it as `range.end`
2720
+ * stretches every node to the start of whatever follows. An editor folding
2721
+ * on that range would hide the first line of the next clause, so the end is
2722
+ * measured from the last token the node consumed. Comments are skipped
2723
+ * because `advance()` steps over them without them belonging to the node.
2724
+ */
2725
+ endPos() {
2726
+ let i = this.pos - 1;
2727
+ while (i >= 0 && this.tokens[i].type === TokenType.Comment)
2728
+ i--;
2729
+ const tok = this.tokens[i];
2730
+ if (!tok)
2731
+ return this.currentPos();
2732
+ return { line: tok.line, column: tok.column + tok.value.length };
2733
+ }
1611
2734
  isAtEnd() {
1612
2735
  return (this.pos >= this.tokens.length || this.current().type === TokenType.EOF);
1613
2736
  }
@@ -1628,6 +2751,10 @@ class Parser {
1628
2751
  this.skipComments();
1629
2752
  return tok;
1630
2753
  }
2754
+ /** The token `i` positions ahead, skipping nothing. */
2755
+ peekPast(i) {
2756
+ return this.tokens[this.pos + i];
2757
+ }
1631
2758
  expect(type) {
1632
2759
  const tok = this.current();
1633
2760
  if (tok.type !== type) {
@@ -1642,7 +2769,7 @@ class Parser {
1642
2769
  * Most KIP keywords only need a word boundary, so `WHERE{...}` is legal.
1643
2770
  * A handful — the statement introducers and the clause keywords whose
1644
2771
  * operand may itself start with a brace or a quote — require real
1645
- * whitespace, which is what keeps `UPSERT{` from reading as a statement.
2772
+ * whitespace, which is what keeps `MUTATE{` from reading as a statement.
1646
2773
  * The distinction is per-keyword-position, not per-keyword, so it lives at
1647
2774
  * the call site rather than in the lexer.
1648
2775
  */
@@ -1658,32 +2785,22 @@ class Parser {
1658
2785
  }
1659
2786
  return this.advance();
1660
2787
  }
1661
- expectVariable() {
1662
- const tok = this.current();
1663
- if (tok.type !== TokenType.Variable) {
1664
- this.error(`Expected variable (e.g., ?name) but got '${tok.value}'`, tok);
1665
- return '?unknown';
1666
- }
1667
- this.advance();
1668
- return tok.value;
1669
- }
1670
- expectString() {
1671
- const tok = this.current();
1672
- if (tok.type !== TokenType.String) {
1673
- this.error(`Expected string literal but got '${tok.value}'`, tok);
1674
- return '';
1675
- }
1676
- this.advance();
1677
- return this.unescapeString(tok.value, tok);
1678
- }
1679
- expectStringValue() {
2788
+ /**
2789
+ * Consumes the second word of a multi-word keyword (`SET FIELDS`,
2790
+ * `AS OF`, `EXPECT VERSION`, `BELIEF SLOT`, ...).
2791
+ *
2792
+ * The grammar joins these with whitespace only. A comment between the words
2793
+ * is not a smaller gap, it is a different token sequence, and reading
2794
+ * `SET//c\nFIELDS` as `SET FIELDS` would accept text the reference grammar
2795
+ * rejects.
2796
+ */
2797
+ expectSecondWord(type, first) {
1680
2798
  const tok = this.current();
1681
- if (tok.type !== TokenType.String) {
1682
- this.error(`Expected quoted string but got '${tok.value}'`, tok);
1683
- return '';
2799
+ const gap = this.source.slice(first.offset + first.value.length, tok.offset);
2800
+ if (tok.type === type && !/^\s+$/.test(gap)) {
2801
+ this.error(`'${first.value} ${tok.value}' must be separated by whitespace only`, tok);
1684
2802
  }
1685
- this.advance();
1686
- return this.unescapeString(tok.value, tok);
2803
+ return this.expect(type);
1687
2804
  }
1688
2805
  expectKeyWithQuoting() {
1689
2806
  const tok = this.current();
@@ -1691,8 +2808,10 @@ class Parser {
1691
2808
  this.advance();
1692
2809
  return { key: this.unescapeString(tok.value, tok), isQuoted: true };
1693
2810
  }
1694
- if (tok.type === TokenType.Identifier ||
1695
- this.isNonAmbiguousKeyword(tok.type)) {
2811
+ // `field_name = identifier | string_literal`, and KIP 2.0 keywords are
2812
+ // contextual: the Spec's own ASSERT sugar writes `by:`, `mode:`, `at:`
2813
+ // and `key:` as object keys.
2814
+ if (isIdentifierLike(tok.type)) {
1696
2815
  this.advance();
1697
2816
  return { key: tok.value, isQuoted: false };
1698
2817
  }
@@ -1706,6 +2825,13 @@ class Parser {
1706
2825
  this.error(`Duplicate ${name} clause`, tok);
1707
2826
  }
1708
2827
  }
2828
+ /** Enforces the canonical statement-level order while still recovering. */
2829
+ checkClauseOrder(order, previous, name, tok) {
2830
+ if (order < previous) {
2831
+ this.error(`${name} clause is out of order`, tok);
2832
+ }
2833
+ return Math.max(order, previous);
2834
+ }
1709
2835
  skipComments() {
1710
2836
  while (this.pos < this.tokens.length &&
1711
2837
  this.current().type === TokenType.Comment) {
@@ -1725,26 +2851,8 @@ class Parser {
1725
2851
  /**
1726
2852
  * Consume the `:` separating an object key from its value. A colon written
1727
2853
  * with no space before an identifier value (e.g. `status:active`) is lexed as
1728
- * a single parameter placeholder token (`:active`), so surface a targeted hint
1729
- * instead of the generic "Expected ':'" message.
1730
- */
1731
- /**
1732
- * Consumes the second word of a two-word keyword (`SET ATTRIBUTES`,
1733
- * `ORDER BY`, `EXPECT VERSION`, ...).
1734
- *
1735
- * The grammar joins these with whitespace only. A comment between the words
1736
- * is not a smaller gap, it is a different token sequence, and reading
1737
- * `SET//c\nMETADATA` as `SET METADATA` would accept text the reference
1738
- * grammar rejects.
2854
+ * a single parameter placeholder token (`:active`), so split it back apart.
1739
2855
  */
1740
- expectSecondWord(type, first) {
1741
- const tok = this.current();
1742
- const gap = this.source.slice(first.offset + first.value.length, tok.offset);
1743
- if (tok.type === type && !/^\s+$/.test(gap)) {
1744
- this.error(`'${first.value} ${tok.value}' must be separated by whitespace only`, tok);
1745
- }
1746
- return this.expect(type);
1747
- }
1748
2856
  expectObjectColon(_key) {
1749
2857
  if (this.check(TokenType.Colon)) {
1750
2858
  this.advance();
@@ -1778,48 +2886,6 @@ class Parser {
1778
2886
  }));
1779
2887
  this.tokens.splice(this.pos, 1, ...retoken);
1780
2888
  }
1781
- isFunctionToken(type) {
1782
- return (type === TokenType.Count ||
1783
- type === TokenType.Sum ||
1784
- type === TokenType.Avg ||
1785
- type === TokenType.Min ||
1786
- type === TokenType.Max ||
1787
- type === TokenType.Contains ||
1788
- type === TokenType.StartsWith ||
1789
- type === TokenType.EndsWith ||
1790
- type === TokenType.Regex ||
1791
- type === TokenType.In ||
1792
- type === TokenType.IsNull ||
1793
- type === TokenType.IsNotNull ||
1794
- type === TokenType.Add ||
1795
- type === TokenType.Mul ||
1796
- type === TokenType.Clamp ||
1797
- type === TokenType.Coalesce);
1798
- }
1799
- /** Keywords that can also serve as property names in dot notation or object keys */
1800
- isNonAmbiguousKeyword(type) {
1801
- return (type === TokenType.Type ||
1802
- type === TokenType.Types ||
1803
- type === TokenType.Attributes ||
1804
- type === TokenType.Metadata ||
1805
- type === TokenType.Propositions ||
1806
- type === TokenType.Identifier ||
1807
- // Allow most keywords as property names since KIP uses snake_case for attrs
1808
- type === TokenType.Asc ||
1809
- type === TokenType.Desc ||
1810
- type === TokenType.Primer ||
1811
- type === TokenType.Domains ||
1812
- type === TokenType.From ||
1813
- type === TokenType.By ||
1814
- type === TokenType.Order ||
1815
- type === TokenType.Set ||
1816
- type === TokenType.With ||
1817
- type === TokenType.Into ||
1818
- type === TokenType.Expect ||
1819
- type === TokenType.Version ||
1820
- type === TokenType.Mode ||
1821
- type === TokenType.Threshold);
1822
- }
1823
2889
  /**
1824
2890
  * Reads the value of a string token.
1825
2891
  *
@@ -1875,21 +2941,65 @@ class Parser {
1875
2941
  code
1876
2942
  });
1877
2943
  }
2944
+ static STATEMENT_STARTERS = new Set([
2945
+ TokenType.Find,
2946
+ TokenType.Mutate,
2947
+ TokenType.Create,
2948
+ TokenType.Upsert,
2949
+ TokenType.Ensure,
2950
+ TokenType.Assert,
2951
+ TokenType.Update,
2952
+ TokenType.Retract,
2953
+ TokenType.Supersede,
2954
+ TokenType.Correct,
2955
+ TokenType.Transition,
2956
+ TokenType.Set,
2957
+ TokenType.Archive,
2958
+ TokenType.Tombstone,
2959
+ TokenType.Purge,
2960
+ TokenType.Merge,
2961
+ TokenType.Describe,
2962
+ TokenType.List,
2963
+ TokenType.Search,
2964
+ TokenType.Verify,
2965
+ TokenType.Validate,
2966
+ TokenType.Preview,
2967
+ TokenType.History,
2968
+ TokenType.Changes,
2969
+ TokenType.Snapshot,
2970
+ TokenType.Export,
2971
+ TokenType.EOF
2972
+ ]);
1878
2973
  recoverToNextStatement() {
1879
- const stmtStarters = new Set([
1880
- TokenType.Find,
1881
- TokenType.Upsert,
1882
- TokenType.Update,
1883
- TokenType.Merge,
1884
- TokenType.Delete,
1885
- TokenType.Describe,
1886
- TokenType.Search,
1887
- TokenType.Export,
1888
- TokenType.EOF
1889
- ]);
1890
- while (!this.isAtEnd() && !stmtStarters.has(this.current().type)) {
2974
+ while (!this.isAtEnd() &&
2975
+ !Parser.STATEMENT_STARTERS.has(this.current().type)) {
1891
2976
  this.pos++;
1892
2977
  }
1893
2978
  }
2979
+ /** Inside MUTATE, recovery stops at the next clause or the closing brace. */
2980
+ recoverToMutationBoundary() {
2981
+ let depth = 0;
2982
+ while (!this.isAtEnd()) {
2983
+ const type = this.current().type;
2984
+ if (type === TokenType.LBrace)
2985
+ depth++;
2986
+ else if (type === TokenType.RBrace) {
2987
+ if (depth === 0)
2988
+ return;
2989
+ depth--;
2990
+ }
2991
+ else if (depth === 0 && Parser.STATEMENT_STARTERS.has(type)) {
2992
+ return;
2993
+ }
2994
+ this.pos++;
2995
+ }
2996
+ }
2997
+ }
2998
+ /** Unwinds a sub-parser that cannot produce a node; `parse` recovers. */
2999
+ class ParseAbort extends Error {
3000
+ constructor() {
3001
+ super('parse aborted');
3002
+ this.name = 'ParseAbort';
3003
+ }
1894
3004
  }
1895
3005
  //# sourceMappingURL=parser.js.map