@ldclabs/kip-lang 0.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -44
  4. package/dist/ast.d.ts +519 -144
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/budget.d.ts +37 -0
  7. package/dist/budget.d.ts.map +1 -0
  8. package/dist/budget.js +105 -0
  9. package/dist/budget.js.map +1 -0
  10. package/dist/diagnostics.d.ts +8 -2
  11. package/dist/diagnostics.d.ts.map +1 -1
  12. package/dist/diagnostics.js +32 -3
  13. package/dist/diagnostics.js.map +1 -1
  14. package/dist/errors.d.ts +29 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/exec-ast.d.ts +679 -0
  19. package/dist/exec-ast.d.ts.map +1 -0
  20. package/dist/exec-ast.js +24 -0
  21. package/dist/exec-ast.js.map +1 -0
  22. package/dist/formatter.d.ts.map +1 -1
  23. package/dist/formatter.js +870 -479
  24. package/dist/formatter.js.map +1 -1
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/lexer.d.ts.map +1 -1
  30. package/dist/lexer.js +46 -33
  31. package/dist/lexer.js.map +1 -1
  32. package/dist/lower.d.ts +17 -0
  33. package/dist/lower.d.ts.map +1 -0
  34. package/dist/lower.js +1566 -0
  35. package/dist/lower.js.map +1 -0
  36. package/dist/parser.d.ts.map +1 -1
  37. package/dist/parser.js +2476 -1152
  38. package/dist/parser.js.map +1 -1
  39. package/dist/semantics.d.ts +11 -7
  40. package/dist/semantics.d.ts.map +1 -1
  41. package/dist/semantics.js +295 -180
  42. package/dist/semantics.js.map +1 -1
  43. package/dist/token.d.ts +130 -40
  44. package/dist/token.d.ts.map +1 -1
  45. package/dist/token.js +264 -83
  46. package/dist/token.js.map +1 -1
  47. package/dist/version.d.ts +13 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +13 -0
  50. package/dist/version.js.map +1 -0
  51. package/package.json +36 -6
  52. package/src/ast.ts +914 -0
  53. package/src/budget.ts +108 -0
  54. package/src/diagnostics.ts +182 -0
  55. package/src/errors.ts +42 -0
  56. package/src/exec-ast.ts +614 -0
  57. package/src/formatter.ts +1339 -0
  58. package/src/index.ts +226 -0
  59. package/src/lexer.ts +459 -0
  60. package/src/lower.ts +2011 -0
  61. package/src/parser.ts +3506 -0
  62. package/src/semantics.ts +392 -0
  63. package/src/token.ts +408 -0
  64. 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);
@@ -20,6 +21,7 @@ class Parser {
20
21
  const start = this.currentPos();
21
22
  this.skipComments();
22
23
  while (!this.isAtEnd()) {
24
+ const before = this.pos;
23
25
  this.skipComments();
24
26
  if (this.isAtEnd())
25
27
  break;
@@ -32,6 +34,11 @@ class Parser {
32
34
  // Error recovery: skip to next statement-level keyword
33
35
  this.recoverToNextStatement();
34
36
  }
37
+ // A sub-parser that rejects its first token reports and returns
38
+ // without consuming it, so a loop keyed on that token would spin
39
+ // forever building diagnostics. Stop as soon as nothing moved.
40
+ if (this.pos === before)
41
+ break;
35
42
  }
36
43
  const end = this.currentPos();
37
44
  return {
@@ -45,817 +52,529 @@ class Parser {
45
52
  parseStatement() {
46
53
  const tok = this.current();
47
54
  switch (tok.type) {
55
+ // KQL
48
56
  case TokenType.Find:
49
57
  return this.parseFindStatement();
58
+ // KML
59
+ case TokenType.Mutate:
60
+ return this.parseMutateStatement();
61
+ case TokenType.Create:
50
62
  case TokenType.Upsert:
51
- return this.parseUpsertStatement();
63
+ case TokenType.Ensure:
64
+ case TokenType.Assert:
52
65
  case TokenType.Update:
53
- 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:
54
74
  case TokenType.Merge:
55
- return this.parseMergeStatement();
56
- case TokenType.Delete:
57
- return this.parseDeleteStatement();
75
+ return this.parseMutationClause();
76
+ // META
58
77
  case TokenType.Describe:
59
78
  return this.parseDescribeStatement();
79
+ case TokenType.List:
80
+ return this.parseListStatement();
60
81
  case TokenType.Search:
61
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();
62
95
  case TokenType.Export:
63
- return this.parseExportStatement();
96
+ return this.parseExportCapsuleStatement();
64
97
  default:
65
- this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, UPDATE, MERGE, DELETE, DESCRIBE, SEARCH, EXPORT)`, tok);
66
- this.advance();
98
+ this.error(`Unexpected token '${tok.value}': expected a KQL, KML or META statement`, tok);
67
99
  return null;
68
100
  }
69
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
+ }
70
139
  // ────────────────────────────────────────────────────────────────────
71
- // FIND
140
+ // KQL — FIND
72
141
  // ────────────────────────────────────────────────────────────────────
73
142
  parseFindStatement() {
143
+ const leadingComments = this.collectLeadingComments();
74
144
  const start = this.currentPos();
75
- const comments = this.collectLeadingComments();
145
+ this.dialect = 'kql';
76
146
  this.expect(TokenType.Find);
77
- const lparen = this.current();
78
147
  this.expect(TokenType.LParen);
79
148
  const projections = [];
80
149
  if (!this.check(TokenType.RParen)) {
81
- projections.push(this.parseExpression());
82
- while (this.match(TokenType.Comma)) {
83
- projections.push(this.parseExpression());
84
- }
150
+ do {
151
+ projections.push(this.parseProjectionExpression());
152
+ } while (this.match(TokenType.Comma));
85
153
  }
86
154
  if (projections.length === 0) {
87
- 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());
88
156
  }
89
157
  this.expect(TokenType.RParen);
90
- let where;
158
+ this.expectKeywordWithSpace(TokenType.Where);
159
+ const where = this.parseWhereClause();
160
+ let asOf;
161
+ let forTime;
162
+ let epistemic;
91
163
  let orderBy;
92
164
  let limit;
93
165
  let cursor;
94
- if (this.check(TokenType.Where)) {
95
- where = this.parseWhereClause();
96
- }
97
- if (this.check(TokenType.Order)) {
98
- orderBy = this.parseOrderBy();
99
- }
100
- if (this.check(TokenType.Limit)) {
101
- limit = this.parseLimitClause();
102
- }
103
- if (this.check(TokenType.Cursor)) {
104
- 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
+ }
105
205
  }
106
206
  return {
107
207
  kind: 'FindStatement',
108
208
  projections,
109
209
  where,
210
+ asOf,
211
+ forTime,
212
+ epistemic,
110
213
  orderBy,
111
214
  limit,
112
215
  cursor,
113
- range: { start, end: this.currentPos() },
114
- leadingComments: comments.length > 0 ? comments : undefined
216
+ range: { start, end: this.endPos() },
217
+ leadingComments: leadingComments.length ? leadingComments : undefined
115
218
  };
116
219
  }
117
- // ────────────────────────────────────────────────────────────────────
118
- // UPSERT
119
- // ────────────────────────────────────────────────────────────────────
120
- parseUpsertStatement() {
220
+ /** `projection_expression = aggregate_expression | expression` */
221
+ parseProjectionExpression() {
222
+ return this.parseExpression();
223
+ }
224
+ parseAsOfClause() {
121
225
  const start = this.currentPos();
122
- const comments = this.collectLeadingComments();
123
- this.expect(TokenType.Upsert);
124
- this.expect(TokenType.LBrace);
125
- const blocks = [];
126
- this.skipComments();
127
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
128
- this.skipComments();
129
- if (this.check(TokenType.Concept)) {
130
- blocks.push(this.parseConceptBlock());
131
- }
132
- else if (this.check(TokenType.Proposition)) {
133
- blocks.push(this.parsePropositionBlock());
134
- }
135
- else if (this.check(TokenType.RBrace)) {
136
- break;
137
- }
138
- else {
139
- this.error(`Expected CONCEPT or PROPOSITION inside UPSERT block`, this.current());
140
- this.advance();
141
- }
142
- this.skipComments();
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';
143
231
  }
144
- this.expect(TokenType.RBrace);
145
- let metadata;
146
- if (this.check(TokenType.With)) {
147
- metadata = this.parseWithMetadata();
232
+ else if (this.match(TokenType.Tx)) {
233
+ basis = 'TX';
234
+ }
235
+ else if (this.match(TokenType.Time)) {
236
+ basis = 'TIME';
237
+ }
238
+ else {
239
+ this.error(`Expected SEQ, TX or TIME after AS OF but got '${this.current().value}'`, this.current());
240
+ basis = 'SEQ';
148
241
  }
242
+ const value = this.parseScalarValue();
149
243
  return {
150
- kind: 'UpsertStatement',
151
- blocks,
152
- metadata,
153
- range: { start, end: this.currentPos() },
154
- leadingComments: comments.length > 0 ? comments : undefined
244
+ kind: 'AsOfClause',
245
+ basis,
246
+ value,
247
+ range: { start, end: this.endPos() }
155
248
  };
156
249
  }
157
- parseConceptBlock() {
250
+ parseForTimeClause() {
158
251
  const start = this.currentPos();
159
- const comments = this.collectLeadingComments();
160
- this.expect(TokenType.Concept);
161
- const handle = this.expectVariable();
162
- this.expect(TokenType.LBrace);
163
- const matcher = this.parseConceptMatcher();
164
- let expectVersion;
165
- if (this.check(TokenType.Expect)) {
166
- expectVersion = this.parseExpectVersion();
167
- }
168
- let setAttributes;
169
- let setPropositions;
170
- let metadata;
171
- this.skipComments();
172
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
173
- this.skipComments();
174
- if (this.check(TokenType.Set)) {
175
- const setStart = this.currentPos();
176
- this.advance(); // skip SET
177
- if (this.check(TokenType.Attributes)) {
178
- this.advance();
179
- setAttributes = this.parseSetAttributesBody(setStart);
180
- }
181
- else if (this.check(TokenType.Propositions)) {
182
- this.advance();
183
- setPropositions = this.parseSetPropositionsBody(setStart);
184
- }
185
- else {
186
- this.error(`Expected ATTRIBUTES or PROPOSITIONS after SET`, this.current());
187
- this.advance();
188
- }
189
- }
190
- else if (this.check(TokenType.With)) {
191
- metadata = this.parseWithMetadata();
192
- }
193
- else if (this.check(TokenType.RBrace)) {
194
- break;
195
- }
196
- else {
197
- this.skipComments();
198
- if (this.check(TokenType.RBrace))
199
- break;
200
- this.error(`Unexpected token '${this.current().value}' in CONCEPT block`, this.current());
201
- this.advance();
202
- }
203
- this.skipComments();
204
- }
205
- this.expect(TokenType.RBrace);
206
- // Concept-level WITH METADATA (outside the CONCEPT braces)
207
- if (!metadata && this.check(TokenType.With)) {
208
- metadata = this.parseWithMetadata();
209
- }
252
+ const first = this.expect(TokenType.For);
253
+ this.expectSecondWord(TokenType.Time, first);
254
+ const value = this.parseScalarValue();
210
255
  return {
211
- kind: 'ConceptBlock',
212
- handle,
213
- matcher,
214
- expectVersion,
215
- setAttributes,
216
- setPropositions,
217
- metadata,
218
- range: { start, end: this.currentPos() },
219
- leadingComments: comments.length > 0 ? comments : undefined
256
+ kind: 'ForTimeClause',
257
+ value,
258
+ range: { start, end: this.endPos() }
220
259
  };
221
260
  }
222
- parsePropositionBlock() {
261
+ parseEpistemicClause() {
223
262
  const start = this.currentPos();
224
- const comments = this.collectLeadingComments();
225
- this.expect(TokenType.Proposition);
226
- let handle;
227
- if (this.check(TokenType.Variable)) {
228
- handle = this.expectVariable();
229
- }
230
- this.expect(TokenType.LBrace);
231
- const proposition = this.parsePropositionPatternBody(undefined);
232
- let expectVersion;
233
- if (this.check(TokenType.Expect)) {
234
- expectVersion = this.parseExpectVersion();
235
- }
236
- let setAttributes;
237
- let metadata;
238
- this.skipComments();
239
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
240
- this.skipComments();
241
- if (this.check(TokenType.Set)) {
242
- const setStart = this.currentPos();
243
- this.advance();
244
- if (this.check(TokenType.Attributes)) {
245
- this.advance();
246
- setAttributes = this.parseSetAttributesBody(setStart);
247
- }
248
- else {
249
- this.error(`Expected ATTRIBUTES after SET in PROPOSITION block`, this.current());
250
- this.advance();
251
- }
252
- }
253
- else if (this.check(TokenType.RBrace)) {
254
- break;
255
- }
256
- else {
257
- this.advance();
258
- }
259
- this.skipComments();
260
- }
261
- this.expect(TokenType.RBrace);
262
- if (this.check(TokenType.With)) {
263
- metadata = this.parseWithMetadata();
264
- }
263
+ const first = this.expect(TokenType.With);
264
+ this.expectSecondWord(TokenType.Epistemic, first);
265
+ const options = this.parseObjectLiteral();
265
266
  return {
266
- kind: 'PropositionBlock',
267
- handle,
268
- id: proposition.id,
269
- subject: proposition.subject,
270
- predicate: proposition.predicate,
271
- object: proposition.object,
272
- expectVersion,
273
- setAttributes,
274
- metadata,
275
- range: { start, end: this.currentPos() },
276
- leadingComments: comments.length > 0 ? comments : undefined
267
+ kind: 'EpistemicClause',
268
+ options,
269
+ range: { start, end: this.endPos() }
277
270
  };
278
271
  }
279
- // ────────────────────────────────────────────────────────────────────
280
- // UPDATE
281
- // ────────────────────────────────────────────────────────────────────
282
- parseUpdateStatement() {
272
+ parseOrderBy() {
283
273
  const start = this.currentPos();
284
- const comments = this.collectLeadingComments();
285
- this.expect(TokenType.Update);
286
- const target = this.expectVariable();
287
- let setAttributes;
288
- let setMetadata;
289
- this.skipComments();
290
- while (this.check(TokenType.Set) && !this.isAtEnd()) {
291
- const setStart = this.currentPos();
292
- this.advance();
293
- if (this.check(TokenType.Attributes)) {
294
- this.advance();
295
- setAttributes = this.parseSetAttributesBody(setStart);
296
- }
297
- else if (this.check(TokenType.Metadata)) {
298
- this.advance();
299
- setMetadata = this.parseSetMetadataBody(setStart);
300
- }
301
- else {
302
- this.error(`Expected ATTRIBUTES or METADATA after SET in UPDATE statement`, this.current());
303
- this.advance();
304
- }
305
- this.skipComments();
306
- }
307
- if (!setAttributes && !setMetadata) {
308
- this.error(`Expected SET ATTRIBUTES or SET METADATA in UPDATE statement`, this.current());
309
- }
310
- const where = this.parseWhereClause();
311
- let limit;
312
- if (this.check(TokenType.Limit)) {
313
- limit = this.parseLimitClause();
314
- }
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));
315
280
  return {
316
- kind: 'UpdateStatement',
317
- target,
318
- setAttributes,
319
- setMetadata,
320
- where,
321
- limit,
322
- range: { start, end: this.currentPos() },
323
- leadingComments: comments.length > 0 ? comments : undefined
281
+ kind: 'OrderByClause',
282
+ items,
283
+ range: { start, end: this.endPos() }
324
284
  };
325
285
  }
326
- // ────────────────────────────────────────────────────────────────────
327
- // MERGE
328
- // ────────────────────────────────────────────────────────────────────
329
- parseMergeStatement() {
286
+ parseOrderItem() {
330
287
  const start = this.currentPos();
331
- const comments = this.collectLeadingComments();
332
- this.expect(TokenType.Merge);
333
- this.expect(TokenType.Concept);
334
- const source = this.expectVariable();
335
- this.expect(TokenType.Into);
336
- const target = this.expectVariable();
337
- 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';
338
294
  return {
339
- kind: 'MergeStatement',
340
- source,
341
- target,
342
- where,
343
- range: { start, end: this.currentPos() },
344
- leadingComments: comments.length > 0 ? comments : undefined
295
+ kind: 'OrderItem',
296
+ expression,
297
+ direction,
298
+ range: { start, end: this.endPos() }
345
299
  };
346
300
  }
347
- // ────────────────────────────────────────────────────────────────────
348
- // DELETE
349
- // ────────────────────────────────────────────────────────────────────
350
- parseDeleteStatement() {
301
+ parseLimitClause() {
351
302
  const start = this.currentPos();
352
- const comments = this.collectLeadingComments();
353
- this.expect(TokenType.Delete);
354
- let deleteType;
355
- let keys;
356
- let target;
357
- let detach = false;
358
- if (this.check(TokenType.Attributes)) {
359
- deleteType = 'ATTRIBUTES';
360
- this.advance();
361
- keys = this.parseDeleteKeySet();
362
- this.expect(TokenType.From);
363
- target = this.expectVariable();
364
- }
365
- else if (this.check(TokenType.Metadata)) {
366
- deleteType = 'METADATA';
367
- this.advance();
368
- keys = this.parseDeleteKeySet();
369
- this.expect(TokenType.From);
370
- target = this.expectVariable();
371
- }
372
- else if (this.check(TokenType.Propositions)) {
373
- deleteType = 'PROPOSITIONS';
374
- this.advance();
375
- target = this.expectVariable();
376
- }
377
- else if (this.check(TokenType.Concept)) {
378
- deleteType = 'CONCEPT';
379
- this.advance();
380
- target = this.expectVariable();
381
- if (this.check(TokenType.Detach)) {
382
- detach = true;
383
- this.advance();
384
- }
385
- else {
386
- this.error(`Expected DETACH after DELETE CONCEPT target '${target}'`, this.current());
387
- }
388
- }
389
- else {
390
- this.error(`Expected ATTRIBUTES, METADATA, PROPOSITIONS, or CONCEPT after DELETE`, this.current());
391
- deleteType = 'ATTRIBUTES';
392
- target = '?unknown';
393
- }
394
- const where = this.parseWhereClause();
303
+ this.expect(TokenType.Limit);
304
+ const value = this.parseScalarValue();
395
305
  return {
396
- kind: 'DeleteStatement',
397
- deleteType,
398
- keys,
399
- target,
400
- detach: detach || undefined,
401
- where,
402
- range: { start, end: this.currentPos() },
403
- leadingComments: comments.length > 0 ? comments : undefined
306
+ kind: 'LimitClause',
307
+ value,
308
+ range: { start, end: this.endPos() }
404
309
  };
405
310
  }
406
- parseDeleteKeySet() {
407
- this.expect(TokenType.LBrace);
408
- const keys = [];
409
- if (!this.check(TokenType.RBrace)) {
410
- keys.push(this.expectString());
411
- while (this.match(TokenType.Comma)) {
412
- if (this.check(TokenType.RBrace))
413
- break;
414
- keys.push(this.expectString());
415
- }
416
- }
417
- this.expect(TokenType.RBrace);
418
- return keys;
311
+ parseCursorClause() {
312
+ const start = this.currentPos();
313
+ this.expect(TokenType.Cursor);
314
+ const value = this.parseScalarValue();
315
+ return {
316
+ kind: 'CursorClause',
317
+ value,
318
+ range: { start, end: this.endPos() }
319
+ };
419
320
  }
420
321
  // ────────────────────────────────────────────────────────────────────
421
- // DESCRIBE
322
+ // WHERE
422
323
  // ────────────────────────────────────────────────────────────────────
423
- parseDescribeStatement() {
324
+ parseWhereClause() {
424
325
  const start = this.currentPos();
425
- const comments = this.collectLeadingComments();
426
- this.expect(TokenType.Describe);
427
- let describeType;
428
- let typeName;
429
- let typeNameValue;
430
- let limit;
431
- let cursor;
432
- if (this.check(TokenType.Primer)) {
433
- describeType = 'PRIMER';
434
- this.advance();
435
- }
436
- else if (this.check(TokenType.Domains)) {
437
- describeType = 'DOMAINS';
438
- this.advance();
439
- }
440
- else if (this.check(TokenType.Concept)) {
441
- this.advance();
442
- if (this.check(TokenType.Types)) {
443
- describeType = 'CONCEPT_TYPES';
444
- this.advance();
445
- }
446
- else if (this.check(TokenType.Type)) {
447
- describeType = 'CONCEPT_TYPE';
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()) {
338
+ const before = this.pos;
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());
448
348
  this.advance();
449
- typeNameValue = this.parseStringOrParameterValue('DESCRIBE CONCEPT TYPE');
450
- typeName =
451
- typeNameValue.kind === 'StringLiteral'
452
- ? typeNameValue.parsed
453
- : typeNameValue.name;
454
- }
455
- else {
456
- this.error(`Expected TYPE or TYPES after DESCRIBE CONCEPT`, this.current());
457
- describeType = 'CONCEPT_TYPES';
458
349
  }
459
350
  }
460
- else if (this.check(TokenType.Proposition)) {
461
- this.advance();
462
- if (this.check(TokenType.Types)) {
463
- describeType = 'PROPOSITION_TYPES';
464
- this.advance();
465
- }
466
- else if (this.check(TokenType.Type)) {
467
- describeType = 'PROPOSITION_TYPE';
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);
468
373
  this.advance();
469
- typeNameValue = this.parseStringOrParameterValue('DESCRIBE PROPOSITION TYPE');
470
- typeName =
471
- typeNameValue.kind === 'StringLiteral'
472
- ? typeNameValue.parsed
473
- : typeNameValue.name;
474
- }
475
- else {
476
- this.error(`Expected TYPE or TYPES after DESCRIBE PROPOSITION`, this.current());
477
- describeType = 'PROPOSITION_TYPES';
478
- }
479
- }
480
- else {
481
- this.error(`Expected PRIMER, DOMAINS, CONCEPT, or PROPOSITION after DESCRIBE`, this.current());
482
- describeType = 'PRIMER';
483
- }
484
- // Only the plural `... TYPES` forms are paginated (§5.1.3 / §5.1.5).
485
- const paginable = describeType === 'CONCEPT_TYPES' || describeType === 'PROPOSITION_TYPES';
486
- if (this.check(TokenType.Limit)) {
487
- if (!paginable) {
488
- this.error(`LIMIT is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
489
- }
490
- limit = this.parseLimitClause();
374
+ return null;
491
375
  }
492
- if (this.check(TokenType.Cursor)) {
493
- if (!paginable) {
494
- this.error(`CURSOR is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
495
- }
496
- cursor = this.parseCursorClause();
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();
497
406
  }
407
+ }
408
+ parseConceptPattern(variable) {
409
+ const explicit = this.match(TokenType.Concept);
410
+ const matcher = this.parseObjectPattern();
498
411
  return {
499
- kind: 'DescribeStatement',
500
- describeType,
501
- typeName,
502
- typeNameValue,
503
- limit,
504
- cursor,
505
- range: { start, end: this.currentPos() },
506
- leadingComments: comments.length > 0 ? comments : undefined
412
+ kind: 'ConceptPattern',
413
+ variable,
414
+ explicit,
415
+ matcher,
416
+ range: { start: variable.range.start, end: this.endPos() }
507
417
  };
508
418
  }
509
- // ────────────────────────────────────────────────────────────────────
510
- // SEARCH
511
- // ────────────────────────────────────────────────────────────────────
512
- parseSearchStatement() {
513
- const start = this.currentPos();
514
- const comments = this.collectLeadingComments();
515
- this.expect(TokenType.Search);
516
- let searchTarget;
517
- if (this.check(TokenType.Concept)) {
518
- searchTarget = 'CONCEPT';
519
- this.advance();
520
- }
521
- else if (this.check(TokenType.Proposition)) {
522
- searchTarget = 'PROPOSITION';
523
- this.advance();
524
- }
525
- else {
526
- this.error(`Expected CONCEPT or PROPOSITION after SEARCH`, this.current());
527
- searchTarget = 'CONCEPT';
528
- }
529
- const termValue = this.parseStringOrParameterValue('SEARCH term');
530
- const term = termValue.kind === 'StringLiteral' ? termValue.parsed : termValue.name;
531
- let withType;
532
- let withTypeValue;
533
- let mode;
534
- let modeValue;
535
- let threshold;
536
- let limit;
537
- while (!this.isAtEnd()) {
538
- if (this.check(TokenType.With)) {
539
- this.advance();
540
- this.expect(TokenType.Type);
541
- withTypeValue = this.parseStringOrParameterValue('SEARCH WITH TYPE');
542
- withType =
543
- withTypeValue.kind === 'StringLiteral'
544
- ? withTypeValue.parsed
545
- : withTypeValue.name;
546
- }
547
- else if (this.check(TokenType.Mode)) {
548
- this.advance();
549
- modeValue = this.parseStringOrParameterValue('SEARCH MODE');
550
- mode =
551
- modeValue.kind === 'StringLiteral' ? modeValue.parsed : modeValue.name;
552
- }
553
- else if (this.check(TokenType.Threshold)) {
554
- threshold = this.parseThresholdClause();
555
- }
556
- else if (this.check(TokenType.Limit)) {
557
- limit = this.parseLimitClause();
558
- }
559
- else {
560
- break;
561
- }
562
- }
563
- return {
564
- kind: 'SearchStatement',
565
- searchTarget,
566
- term,
567
- termValue,
568
- withType,
569
- withTypeValue,
570
- mode,
571
- modeValue,
572
- threshold,
573
- limit,
574
- range: { start, end: this.currentPos() },
575
- leadingComments: comments.length > 0 ? comments : undefined
576
- };
577
- }
578
- // ────────────────────────────────────────────────────────────────────
579
- // EXPORT
580
- // ────────────────────────────────────────────────────────────────────
581
- parseExportStatement() {
582
- const start = this.currentPos();
583
- const comments = this.collectLeadingComments();
584
- this.expect(TokenType.Export);
585
- const target = this.expectVariable();
586
- const where = this.parseWhereClause();
587
- let limit;
588
- if (this.check(TokenType.Limit)) {
589
- limit = this.parseLimitClause();
590
- }
591
- return {
592
- kind: 'ExportStatement',
593
- target,
594
- where,
595
- limit,
596
- range: { start, end: this.currentPos() },
597
- leadingComments: comments.length > 0 ? comments : undefined
598
- };
599
- }
600
- // ────────────────────────────────────────────────────────────────────
601
- // WHERE clause and patterns
602
- // ────────────────────────────────────────────────────────────────────
603
- parseWhereClause() {
604
- const start = this.currentPos();
605
- this.expect(TokenType.Where);
606
- this.expect(TokenType.LBrace);
607
- const patterns = this.parseWherePatterns();
608
- this.expect(TokenType.RBrace);
419
+ parsePropositionPattern(variable) {
420
+ const start = variable ? variable.range.start : this.currentPos();
421
+ const explicit = this.match(TokenType.Proposition);
422
+ const tuple = this.parsePropositionTuple();
609
423
  return {
610
- kind: 'WhereClause',
611
- patterns,
612
- range: { start, end: this.currentPos() }
424
+ kind: 'PropositionPattern',
425
+ variable,
426
+ explicit,
427
+ tuple,
428
+ range: { start, end: this.endPos() }
613
429
  };
614
430
  }
615
- parseWherePatterns() {
616
- const patterns = [];
617
- this.skipComments();
618
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
619
- this.skipComments();
620
- if (this.check(TokenType.RBrace))
621
- break;
622
- const pattern = this.parseWherePattern();
623
- if (pattern)
624
- patterns.push(pattern);
625
- this.skipComments();
626
- }
627
- return patterns;
628
- }
629
- parseWherePattern() {
630
- this.skipComments();
631
- const tok = this.current();
632
- if (tok.type === TokenType.Filter) {
633
- return this.parseFilterClause();
634
- }
635
- if (tok.type === TokenType.Not) {
636
- return this.parseNotClause();
637
- }
638
- if (tok.type === TokenType.Optional) {
639
- return this.parseOptionalClause();
640
- }
641
- if (tok.type === TokenType.Union) {
642
- return this.parseUnionClause();
643
- }
644
- // Variable: could be concept pattern or proposition pattern
645
- if (tok.type === TokenType.Variable) {
646
- return this.parseVariableLeadingPattern();
647
- }
648
- // Opening ( = proposition pattern without variable binding
649
- if (tok.type === TokenType.LParen) {
650
- return this.parsePropositionPatternBody(undefined);
651
- }
652
- this.error(`Unexpected token '${tok.value}' in WHERE clause`, tok);
653
- this.advance();
654
- return null;
655
- }
656
- parseVariableLeadingPattern() {
657
- // ?var could be followed by:
658
- // { ... } => concept pattern
659
- // ( ... ) => proposition pattern
660
- const start = this.currentPos();
661
- const variable = this.expectVariable();
662
- if (this.check(TokenType.LBrace)) {
663
- return this.parseConceptPatternBody(variable, start);
664
- }
665
- if (this.check(TokenType.LParen)) {
666
- return this.parsePropositionPatternBody(variable, start);
667
- }
668
- // Just a variable reference as a standalone concept pattern without matcher
669
- // This occurs in WHERE like: ?drug {type: "Drug"}
670
- this.error(`Expected '{' or '(' after variable '${variable}' in WHERE clause`, this.current());
431
+ parseAssertionPattern(variable) {
432
+ this.expect(TokenType.Assertion);
433
+ const matcher = this.parseObjectPattern();
671
434
  return {
672
- kind: 'ConceptPattern',
435
+ kind: 'AssertionPattern',
673
436
  variable,
674
- matcher: {
675
- kind: 'ConceptMatcher',
676
- entries: [],
677
- range: { start, end: this.currentPos() }
678
- },
679
- range: { start, end: this.currentPos() }
437
+ matcher,
438
+ range: { start: variable.range.start, end: this.endPos() }
680
439
  };
681
440
  }
682
- parseConceptPatternBody(variable, start) {
683
- const matcher = this.parseConceptMatcher();
441
+ parseEvidencePattern(variable) {
442
+ this.expect(TokenType.Evidence);
443
+ const matcher = this.parseObjectPattern();
684
444
  return {
685
- kind: 'ConceptPattern',
445
+ kind: 'EvidencePattern',
686
446
  variable,
687
447
  matcher,
688
- range: { start, end: this.currentPos() }
448
+ range: { start: variable.range.start, end: this.endPos() }
689
449
  };
690
450
  }
691
- parseConceptMatcher() {
692
- const start = this.currentPos();
693
- this.expect(TokenType.LBrace);
694
- const entries = this.parseObjectEntries();
695
- this.expect(TokenType.RBrace);
451
+ parseActivityPattern(variable) {
452
+ this.expect(TokenType.Activity);
453
+ const matcher = this.parseObjectPattern();
696
454
  return {
697
- kind: 'ConceptMatcher',
698
- entries,
699
- range: { start, end: this.currentPos() }
455
+ kind: 'ActivityPattern',
456
+ variable,
457
+ matcher,
458
+ range: { start: variable.range.start, end: this.endPos() }
700
459
  };
701
460
  }
702
- parsePropositionPatternBody(variable, start = this.currentPos()) {
461
+ parseStructuralPattern(variable) {
462
+ const start = variable ? variable.range.start : this.currentPos();
463
+ this.expect(TokenType.Structural);
703
464
  this.expect(TokenType.LParen);
704
- if (this.isIdMatcherStart()) {
705
- const id = this.parseIdMatcherValue();
706
- this.expect(TokenType.RParen);
707
- return {
708
- kind: 'PropositionPattern',
709
- variable,
710
- id,
711
- range: { start, end: this.currentPos() }
712
- };
713
- }
714
- const subject = this.parsePropositionEndpoint();
465
+ const subject = this.parseTerm();
715
466
  this.expect(TokenType.Comma);
716
- const predicate = this.parsePredicateExpr();
467
+ const field = this.parseSchemaSymbol();
717
468
  this.expect(TokenType.Comma);
718
- const object = this.parsePropositionEndpoint();
469
+ const object = this.parseTerm();
719
470
  this.expect(TokenType.RParen);
720
471
  return {
721
- kind: 'PropositionPattern',
472
+ kind: 'StructuralPattern',
722
473
  variable,
723
474
  subject,
724
- predicate,
475
+ field,
725
476
  object,
726
- range: { start, end: this.currentPos() }
477
+ range: { start, end: this.endPos() }
727
478
  };
728
479
  }
729
- parsePropositionEndpoint() {
730
- // Could be: ?var, ?var {...}, ?var (...), {...}, or nested (...)
731
- if (this.check(TokenType.Variable)) {
732
- const start = this.currentPos();
733
- const name = this.expectVariable();
734
- if (this.check(TokenType.LBrace)) {
735
- return this.parseConceptPatternBody(name, start);
736
- }
737
- if (this.check(TokenType.LParen)) {
738
- return this.parsePropositionPatternBody(name, start);
739
- }
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);
740
492
  return {
741
- kind: 'VariableRef',
742
- name,
743
- range: { start, end: this.currentPos() }
493
+ kind: 'BeliefSlotPattern',
494
+ variable,
495
+ subject,
496
+ predicate,
497
+ range: { start, end: this.endPos() }
744
498
  };
745
499
  }
746
- if (this.check(TokenType.LBrace)) {
747
- const start = this.currentPos();
748
- const matcher = this.parseConceptMatcher();
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);
749
507
  return {
750
- kind: 'ConceptPattern',
751
- matcher,
752
- range: { start, end: this.currentPos() }
508
+ kind: 'BeliefPattern',
509
+ variable,
510
+ proposition,
511
+ range: { start, end: this.endPos() }
753
512
  };
754
513
  }
755
- if (this.check(TokenType.LParen)) {
756
- return this.parsePropositionPatternBody(undefined);
757
- }
758
- this.error(`Expected variable, concept pattern, or proposition pattern`, this.current());
759
- const start = this.currentPos();
760
- return {
761
- kind: 'VariableRef',
762
- name: '?unknown',
763
- range: { start, end: start }
764
- };
765
- }
766
- parsePredicateExpr() {
767
- const start = this.currentPos();
768
- if (this.check(TokenType.Variable)) {
769
- const pred = this.parsePredicateVariable();
770
- if (this.check(TokenType.LBrace)) {
771
- this.error(`Predicate variables cannot use hop ranges; use a quoted predicate literal for path traversal`, this.current());
772
- this.parseHopRange();
773
- }
774
- if (this.check(TokenType.Pipe)) {
775
- this.error(`Predicate variables cannot be used in predicate alternations`, this.current());
776
- while (this.match(TokenType.Pipe)) {
777
- if (this.check(TokenType.String)) {
778
- this.parsePredicateLiteral();
779
- }
780
- else if (this.check(TokenType.Variable)) {
781
- this.parsePredicateVariable();
782
- }
783
- else {
784
- break;
785
- }
786
- }
787
- }
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);
788
523
  return {
789
- ...pred,
790
- range: { start, end: this.currentPos() }
524
+ kind: 'BeliefPattern',
525
+ variable,
526
+ propositionId,
527
+ range: { start, end: this.endPos() }
791
528
  };
792
529
  }
793
- const first = this.parsePredicateLiteral();
794
- // Check for alternation: "pred1" | "pred2"
795
- if (this.check(TokenType.Pipe)) {
796
- const predicates = [first];
797
- while (this.match(TokenType.Pipe)) {
798
- predicates.push(this.parsePredicateLiteral());
799
- }
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);
800
540
  return {
801
- kind: 'PredicateAlternation',
802
- predicates,
803
- range: { start, end: this.currentPos() }
541
+ kind: 'BeliefPattern',
542
+ variable,
543
+ propositionId,
544
+ range: { start, end: this.endPos() }
804
545
  };
805
546
  }
806
- return first;
807
- }
808
- parsePredicateVariable() {
809
- const start = this.currentPos();
810
- const name = this.expectVariable();
811
- return {
812
- kind: 'PredicateVariable',
813
- name,
814
- range: { start, end: this.currentPos() }
815
- };
816
- }
817
- parsePredicateLiteral() {
818
- const start = this.currentPos();
819
- const value = this.expectStringValue();
820
- // Check for hop range: {m,n} {m,} {m}
821
- let hopRange;
822
- if (this.check(TokenType.LBrace)) {
823
- hopRange = this.parseHopRange();
824
- }
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);
825
554
  return {
826
- kind: 'PredicateLiteral',
827
- value,
828
- hopRange,
829
- range: { start, end: this.currentPos() }
555
+ kind: 'BeliefPattern',
556
+ variable,
557
+ subject,
558
+ predicate,
559
+ object,
560
+ range: { start, end: this.endPos() }
830
561
  };
831
562
  }
832
- parseHopRange() {
833
- const start = this.currentPos();
834
- this.expect(TokenType.LBrace);
835
- const minTok = this.current();
836
- if (minTok.type !== TokenType.Number) {
837
- this.error(`Expected number in hop range`, minTok);
838
- }
839
- const min = Number(minTok.value);
840
- this.advance();
841
- let max;
842
- if (this.match(TokenType.Comma)) {
843
- if (this.check(TokenType.Number)) {
844
- max = Number(this.current().value);
845
- this.advance();
846
- }
847
- // else: {m,} means unbounded
848
- }
849
- else {
850
- max = min; // {m} means exactly m
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
+ });
851
577
  }
852
- this.expect(TokenType.RBrace);
853
- return {
854
- kind: 'HopRange',
855
- min,
856
- max,
857
- range: { start, end: this.currentPos() }
858
- };
859
578
  }
860
579
  parseFilterClause() {
861
580
  const start = this.currentPos();
@@ -866,7 +585,7 @@ class Parser {
866
585
  return {
867
586
  kind: 'FilterClause',
868
587
  expression,
869
- range: { start, end: this.currentPos() }
588
+ range: { start, end: this.endPos() }
870
589
  };
871
590
  }
872
591
  parseNotClause() {
@@ -878,7 +597,7 @@ class Parser {
878
597
  return {
879
598
  kind: 'NotClause',
880
599
  patterns,
881
- range: { start, end: this.currentPos() }
600
+ range: { start, end: this.endPos() }
882
601
  };
883
602
  }
884
603
  parseOptionalClause() {
@@ -890,7 +609,7 @@ class Parser {
890
609
  return {
891
610
  kind: 'OptionalClause',
892
611
  patterns,
893
- range: { start, end: this.currentPos() }
612
+ range: { start, end: this.endPos() }
894
613
  };
895
614
  }
896
615
  parseUnionClause() {
@@ -902,272 +621,1535 @@ class Parser {
902
621
  return {
903
622
  kind: 'UnionClause',
904
623
  patterns,
905
- range: { start, end: this.currentPos() }
624
+ range: { start, end: this.endPos() }
906
625
  };
907
626
  }
908
627
  // ────────────────────────────────────────────────────────────────────
909
- // SET ATTRIBUTES / SET PROPOSITIONS / WITH METADATA
628
+ // Raw semantic tuples
910
629
  // ────────────────────────────────────────────────────────────────────
911
- parseSetAttributesBody(start) {
912
- this.expect(TokenType.LBrace);
913
- const entries = this.parseObjectEntries();
914
- this.expect(TokenType.RBrace);
915
- return {
916
- kind: 'SetAttributes',
917
- entries,
918
- range: { start, end: this.currentPos() }
919
- };
920
- }
921
- parseSetMetadataBody(start) {
922
- this.expect(TokenType.LBrace);
923
- const entries = this.parseObjectEntries();
924
- this.expect(TokenType.RBrace);
925
- return {
926
- kind: 'SetMetadata',
927
- entries,
928
- range: { start, end: this.currentPos() }
929
- };
930
- }
931
- parseSetPropositionsBody(start) {
932
- this.expect(TokenType.LBrace);
933
- const items = [];
934
- this.skipComments();
935
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
936
- this.skipComments();
937
- if (this.check(TokenType.RBrace))
938
- break;
939
- items.push(this.parsePropositionItem());
940
- this.skipComments();
941
- }
942
- this.expect(TokenType.RBrace);
943
- return {
944
- kind: 'SetPropositions',
945
- items,
946
- range: { start, end: this.currentPos() }
947
- };
948
- }
949
- parsePropositionItem() {
630
+ parsePropositionTuple() {
950
631
  const start = this.currentPos();
951
632
  this.expect(TokenType.LParen);
952
- const predicate = this.expectStringValue();
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();
953
649
  this.expect(TokenType.Comma);
954
- const target = this.parsePropositionEndpoint();
650
+ const predicate = this.parseRawPredicateExpression();
651
+ this.expect(TokenType.Comma);
652
+ const object = this.parseTerm();
955
653
  this.expect(TokenType.RParen);
956
- let metadata;
957
- if (this.check(TokenType.With)) {
958
- metadata = this.parseWithMetadata();
959
- }
960
654
  return {
961
- kind: 'PropositionItem',
655
+ kind: 'PropositionTuple',
656
+ subject,
962
657
  predicate,
963
- target,
964
- metadata,
965
- range: { start, end: this.currentPos() }
658
+ object,
659
+ range: { start, end: this.endPos() }
966
660
  };
967
661
  }
968
- isIdMatcherStart() {
969
- const next = this.tokens[this.pos + 1];
970
- return this.isIdKeyToken(this.current()) && next?.type === TokenType.Colon;
971
- }
972
- parseIdMatcherValue() {
973
- const keyTok = this.current();
974
- if (!this.isIdKeyToken(keyTok)) {
975
- this.error(`Expected id matcher key but got '${keyTok.value}'`, keyTok);
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();
976
696
  }
977
- this.advance();
978
- this.expect(TokenType.Colon);
979
- return this.parseStringOrParameterValue('proposition id');
980
697
  }
981
- parseStringOrParameterValue(context) {
698
+ /** `predicate_atom = string_literal | parameter | variable` */
699
+ parsePredicateAtom() {
982
700
  const tok = this.current();
983
- const start = this.currentPos();
984
701
  if (tok.type === TokenType.String) {
985
- this.advance();
986
- return {
987
- kind: 'StringLiteral',
988
- value: tok.value,
989
- parsed: this.unescapeString(tok.value),
990
- range: { start, end: this.currentPos() }
991
- };
702
+ return this.parseStringLiteral();
992
703
  }
993
704
  if (tok.type === TokenType.Parameter) {
994
- this.advance();
995
- return {
996
- kind: 'ParameterRef',
997
- name: tok.value,
998
- range: { start, end: this.currentPos() }
999
- };
705
+ return this.parseParameterRef();
1000
706
  }
1001
- this.error(`Expected string or parameter for ${context}`, tok);
1002
- return {
1003
- kind: 'StringLiteral',
1004
- value: '""',
1005
- parsed: '',
1006
- range: { start, end: start }
1007
- };
1008
- }
1009
- isIdKeyToken(tok) {
1010
- return ((tok.type === TokenType.Identifier && tok.value === 'id') ||
1011
- (tok.type === TokenType.String && this.unescapeString(tok.value) === 'id'));
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();
1012
712
  }
1013
- parseWithMetadata() {
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() {
1014
721
  const start = this.currentPos();
1015
- this.expect(TokenType.With);
1016
- this.expect(TokenType.Metadata);
1017
- this.expect(TokenType.LBrace);
1018
- const entries = this.parseObjectEntries();
1019
- this.expect(TokenType.RBrace);
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
+ }
1020
731
  return {
1021
- kind: 'WithMetadata',
1022
- entries,
1023
- range: { start, end: this.currentPos() }
732
+ kind: 'RawPredicateExpression',
733
+ atoms,
734
+ range: { start, end: this.endPos() }
1024
735
  };
1025
736
  }
1026
- parseExpectVersion() {
737
+ parsePredicatePathAtom() {
1027
738
  const start = this.currentPos();
1028
- this.expect(TokenType.Expect);
1029
- this.expect(TokenType.Version);
1030
- const value = this.parseNumberOrParameterValue('EXPECT VERSION');
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);
745
+ }
746
+ quantifier = this.parsePathQuantifier();
747
+ }
1031
748
  return {
1032
- kind: 'ExpectVersion',
1033
- value,
1034
- range: { start, end: this.currentPos() }
749
+ kind: 'PredicatePathAtom',
750
+ atom,
751
+ quantifier,
752
+ range: { start, end: this.endPos() }
1035
753
  };
1036
754
  }
1037
- // ────────────────────────────────────────────────────────────────────
1038
- // ORDER BY, LIMIT, CURSOR
1039
- // ────────────────────────────────────────────────────────────────────
1040
- parseOrderBy() {
755
+ parsePathQuantifier() {
1041
756
  const start = this.currentPos();
1042
- this.expect(TokenType.Order);
1043
- this.expect(TokenType.By);
1044
- const keys = [];
1045
- keys.push(this.parseOrderByKey());
1046
- while (this.match(TokenType.Comma)) {
1047
- keys.push(this.parseOrderByKey());
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());
1048
773
  }
1049
- const first = keys[0];
1050
774
  return {
1051
- kind: 'OrderByClause',
1052
- keys,
1053
- expression: first.expression,
1054
- direction: first.direction,
1055
- range: { start, end: this.currentPos() }
775
+ kind: 'PathQuantifier',
776
+ min,
777
+ max,
778
+ hasComma,
779
+ range: { start, end: this.endPos() }
1056
780
  };
1057
781
  }
1058
- parseOrderByKey() {
1059
- const start = this.currentPos();
1060
- const expression = this.parseExpression();
1061
- let direction = 'ASC';
1062
- if (this.check(TokenType.Asc)) {
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);
1063
792
  this.advance();
1064
- direction = 'ASC';
793
+ return 0;
1065
794
  }
1066
- else if (this.check(TokenType.Desc)) {
1067
- this.advance();
1068
- direction = 'DESC';
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());
820
+ this.advance();
821
+ continue;
822
+ }
823
+ try {
824
+ clauses.push(this.parseMutationClause());
825
+ }
826
+ catch {
827
+ this.recoverToMutationBoundary();
828
+ }
829
+ if (this.pos === before)
830
+ break;
1069
831
  }
832
+ this.expect(TokenType.RBrace);
1070
833
  return {
1071
- kind: 'OrderByKey',
1072
- expression,
1073
- direction,
1074
- range: { start, end: this.currentPos() }
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
+ }
971
+ }
972
+ else {
973
+ this.error(`Unexpected token '${tok.value}' in ${kind}`, tok);
974
+ this.advance();
975
+ }
976
+ if (this.pos === before)
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();
1053
+ }
1054
+ if (this.pos === before)
1055
+ break;
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
+ }
1097
+ return {
1098
+ kind: 'AssertStatement',
1099
+ handle,
1100
+ tuple,
1101
+ assignments,
1102
+ superseding,
1103
+ range: { start, end: this.endPos() },
1104
+ leadingComments: leadingComments.length ? leadingComments : undefined
1075
1105
  };
1076
1106
  }
1077
- parseThresholdClause() {
1107
+ // ────────────────────────────────────────────────────────────────────
1108
+ // KML — clause vocabulary
1109
+ // ────────────────────────────────────────────────────────────────────
1110
+ parseTypeClause() {
1111
+ const start = this.currentPos();
1112
+ this.expect(TokenType.Type);
1113
+ const value = this.parseSchemaSymbol();
1114
+ return { kind: 'TypeClause', value, range: { start, end: this.endPos() } };
1115
+ }
1116
+ parseClientKeyClause() {
1078
1117
  const start = this.currentPos();
1079
- this.expect(TokenType.Threshold);
1080
- const value = this.parseNumberOrParameterValue('THRESHOLD');
1118
+ const client = this.expect(TokenType.Client);
1119
+ this.expectSecondWord(TokenType.Key, client);
1120
+ const value = this.parseScalarValue();
1081
1121
  return {
1082
- kind: 'ThresholdClause',
1122
+ kind: 'ClientKeyClause',
1083
1123
  value,
1084
- range: { start, end: this.currentPos() }
1124
+ range: { start, end: this.endPos() }
1085
1125
  };
1086
1126
  }
1087
- parseNumberOrParameterValue(context) {
1127
+ parseNameClause() {
1128
+ const start = this.currentPos();
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();
1137
+ return {
1138
+ kind: 'MatchClause',
1139
+ pattern,
1140
+ range: { start, end: this.endPos() }
1141
+ };
1142
+ }
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);
1088
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 = [];
1192
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1193
+ const before = this.pos;
1194
+ this.skipComments();
1195
+ if (this.check(TokenType.RBrace) || this.isAtEnd())
1196
+ break;
1197
+ assignments.push(this.parseStructuralAssignment());
1198
+ if (this.pos === before)
1199
+ break;
1200
+ }
1201
+ this.expect(TokenType.RBrace);
1202
+ return {
1203
+ kind: 'SetStructuralClause',
1204
+ assignments,
1205
+ range: { start, end: this.endPos() }
1206
+ };
1207
+ }
1208
+ parseStructuralAssignment() {
1089
1209
  const start = this.currentPos();
1090
- let value;
1091
- if (tok.type === TokenType.Number) {
1092
- value = {
1093
- kind: 'NumberLiteral',
1094
- value: Number(tok.value),
1095
- raw: tok.value,
1096
- range: { start, end: start }
1210
+ this.expect(TokenType.LParen);
1211
+ const field = this.parseSchemaSymbol();
1212
+ this.expect(TokenType.Comma);
1213
+ const value = this.parseMutationValue();
1214
+ this.expect(TokenType.RParen);
1215
+ const options = this.check(TokenType.LBrace)
1216
+ ? this.parseObjectLiteral()
1217
+ : undefined;
1218
+ return {
1219
+ kind: 'StructuralAssignment',
1220
+ field,
1221
+ value,
1222
+ options,
1223
+ range: { start, end: this.endPos() }
1224
+ };
1225
+ }
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();
1233
+ return {
1234
+ kind: 'UnsetAttributesClause',
1235
+ fields,
1236
+ range: { start, end: this.endPos() }
1097
1237
  };
1098
- this.advance();
1099
- value.range.end = this.currentPos();
1100
- return value;
1101
1238
  }
1102
- if (tok.type === TokenType.Parameter) {
1103
- value = {
1104
- kind: 'ParameterRef',
1105
- name: tok.value,
1106
- range: { start, end: start }
1239
+ if (tok.type === TokenType.Facet) {
1240
+ this.expectSecondWord(TokenType.Facet, unset);
1241
+ const facet = this.parseSchemaSymbol();
1242
+ const fields = this.parseUnsetFieldSet();
1243
+ return {
1244
+ kind: 'UnsetFacetClause',
1245
+ facet,
1246
+ fields,
1247
+ range: { start, end: this.endPos() }
1107
1248
  };
1108
- this.advance();
1109
- value.range.end = this.currentPos();
1110
- return value;
1111
1249
  }
1112
- this.error(`Expected number or parameter after ${context}`, tok);
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;
1264
+ }
1265
+ this.expect(TokenType.RBrace);
1266
+ return {
1267
+ kind: 'UnsetStructuralClause',
1268
+ removals,
1269
+ range: { start, end: this.endPos() }
1270
+ };
1271
+ }
1272
+ this.error(`Expected ATTRIBUTES, FACET or STRUCTURAL after UNSET but got '${tok.value}'`, tok);
1273
+ throw new ParseAbort();
1274
+ }
1275
+ parseStructuralRemoval() {
1276
+ const start = this.currentPos();
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);
1282
+ if (this.check(TokenType.LBrace)) {
1283
+ this.error('UNSET STRUCTURAL removes a reference by (field, target); it takes no options object', this.current());
1284
+ }
1113
1285
  return {
1114
- kind: 'NumberLiteral',
1115
- value: 0,
1116
- raw: '0',
1117
- range: { start, end: start }
1286
+ kind: 'StructuralRemoval',
1287
+ field,
1288
+ value,
1289
+ range: { start, end: this.endPos() }
1118
1290
  };
1119
1291
  }
1120
- parseLimitClause() {
1292
+ parseUnsetFieldSet() {
1293
+ this.expect(TokenType.LBrace);
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));
1308
+ }
1309
+ this.expect(TokenType.RBrace);
1310
+ return fields;
1311
+ }
1312
+ parseExpectVersionClause() {
1121
1313
  const start = this.currentPos();
1122
- this.expect(TokenType.Limit);
1123
- const value = this.parseNumberOrParameterValue('LIMIT');
1314
+ const expect = this.expect(TokenType.Expect);
1315
+ this.expectSecondWord(TokenType.Version, expect);
1316
+ const value = this.parseScalarValue();
1124
1317
  return {
1125
- kind: 'LimitClause',
1318
+ kind: 'ExpectVersionClause',
1126
1319
  value,
1127
- range: { start, end: this.currentPos() }
1320
+ range: { start, end: this.endPos() }
1128
1321
  };
1129
1322
  }
1130
- parseCursorClause() {
1323
+ parseExpectStateClause() {
1131
1324
  const start = this.currentPos();
1132
- this.expect(TokenType.Cursor);
1325
+ const expect = this.expect(TokenType.Expect);
1326
+ this.expectSecondWord(TokenType.State, expect);
1327
+ const value = this.parseScalarValue();
1328
+ return {
1329
+ kind: 'ExpectStateClause',
1330
+ value,
1331
+ range: { start, end: this.endPos() }
1332
+ };
1333
+ }
1334
+ // ────────────────────────────────────────────────────────────────────
1335
+ // KML — UPDATE
1336
+ // ────────────────────────────────────────────────────────────────────
1337
+ parseUpdateStatement() {
1338
+ const leadingComments = this.collectLeadingComments();
1339
+ const start = this.currentPos();
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;
1376
+ return {
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
1385
+ };
1386
+ }
1387
+ // ────────────────────────────────────────────────────────────────────
1388
+ // KML — lifecycle and correction
1389
+ // ────────────────────────────────────────────────────────────────────
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;
1406
+ return {
1407
+ kind: 'RetractAssertionStatement',
1408
+ target,
1409
+ where,
1410
+ limit,
1411
+ expectState,
1412
+ range: { start, end: this.endPos() },
1413
+ leadingComments: leadingComments.length ? leadingComments : undefined
1414
+ };
1415
+ }
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;
1427
+ return {
1428
+ kind: 'SupersedeAssertionStatement',
1429
+ target,
1430
+ by,
1431
+ expectState,
1432
+ range: { start, end: this.endPos() },
1433
+ leadingComments: leadingComments.length ? leadingComments : undefined
1434
+ };
1435
+ }
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());
1474
+ break;
1475
+ }
1476
+ }
1477
+ const expectState = this.check(TokenType.Expect)
1478
+ ? this.parseExpectStateClause()
1479
+ : undefined;
1480
+ return {
1481
+ kind: 'TransitionActivityStatement',
1482
+ target,
1483
+ to,
1484
+ finalize,
1485
+ expectState,
1486
+ range: { start, end: this.endPos() },
1487
+ leadingComments: leadingComments.length ? leadingComments : undefined
1488
+ };
1489
+ }
1490
+ // ────────────────────────────────────────────────────────────────────
1491
+ // KML — retention and removal
1492
+ // ────────────────────────────────────────────────────────────────────
1493
+ parseSetRetention() {
1494
+ const leadingComments = this.collectLeadingComments();
1495
+ const start = this.currentPos();
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();
1505
+ }
1506
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
1507
+ const expectVersion = this.check(TokenType.Expect)
1508
+ ? this.parseExpectVersionClause()
1509
+ : undefined;
1510
+ return {
1511
+ kind: 'SetRetentionStatement',
1512
+ target,
1513
+ assignments,
1514
+ where,
1515
+ limit,
1516
+ expectVersion,
1517
+ range: { start, end: this.endPos() },
1518
+ leadingComments: leadingComments.length ? leadingComments : undefined
1519
+ };
1520
+ }
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
+ };
1532
+ }
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
+ };
1544
+ }
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();
1556
+ }
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
+ };
1569
+ }
1570
+ parsePurgeStatement() {
1571
+ const leadingComments = this.collectLeadingComments();
1572
+ const start = this.currentPos();
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();
1580
+ }
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);
1596
+ }
1597
+ return {
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
1606
+ };
1607
+ }
1608
+ parseMergeConcept() {
1609
+ const leadingComments = this.collectLeadingComments();
1610
+ const start = this.currentPos();
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;
1625
+ return {
1626
+ kind: 'MergeConceptStatement',
1627
+ source,
1628
+ into,
1629
+ where,
1630
+ expectVersion,
1631
+ range: { start, end: this.endPos() },
1632
+ leadingComments: leadingComments.length ? leadingComments : undefined
1633
+ };
1634
+ }
1635
+ // ────────────────────────────────────────────────────────────────────
1636
+ // META — DESCRIBE
1637
+ // ────────────────────────────────────────────────────────────────────
1638
+ parseDescribeStatement() {
1639
+ const leadingComments = this.collectLeadingComments();
1640
+ const start = this.currentPos();
1641
+ const describe = this.expectKeywordWithSpace(TokenType.Describe);
1133
1642
  const tok = this.current();
1134
- let value;
1135
- if (tok.type === TokenType.String) {
1136
- value = {
1137
- kind: 'StringLiteral',
1138
- value: tok.value,
1139
- parsed: this.unescapeString(tok.value),
1140
- range: { start: this.currentPos(), end: this.currentPos() }
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();
1761
+ const start = this.currentPos();
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;
1811
+ return {
1812
+ kind: 'ListStatement',
1813
+ target,
1814
+ status,
1815
+ limit,
1816
+ cursor,
1817
+ range: { start, end: this.endPos() },
1818
+ leadingComments: leadingComments.length ? leadingComments : undefined
1819
+ };
1820
+ }
1821
+ // ────────────────────────────────────────────────────────────────────
1822
+ // META — SEARCH
1823
+ // ────────────────────────────────────────────────────────────────────
1824
+ parseSearchStatement() {
1825
+ const leadingComments = this.collectLeadingComments();
1826
+ const start = this.currentPos();
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
+ }
1877
+ }
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;
1892
+ return {
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
1905
+ };
1906
+ }
1907
+ // ────────────────────────────────────────────────────────────────────
1908
+ // META — VERIFY / VALIDATE / PREVIEW
1909
+ // ────────────────────────────────────────────────────────────────────
1910
+ parseVerifyStatement() {
1911
+ const leadingComments = this.collectLeadingComments();
1912
+ const start = this.currentPos();
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();
1942
+ }
1943
+ return {
1944
+ kind: 'VerifyStatement',
1945
+ target,
1946
+ value: this.parseScalarValue(),
1947
+ range: { start, end: this.endPos() },
1948
+ leadingComments: leadingComments.length ? leadingComments : undefined
1949
+ };
1950
+ }
1951
+ parseValidateStatement() {
1952
+ const leadingComments = this.collectLeadingComments();
1953
+ const start = this.currentPos();
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;
1990
+ return {
1991
+ kind: 'ValidateStatement',
1992
+ target,
1993
+ value,
1994
+ options,
1995
+ range: { start, end: this.endPos() },
1996
+ leadingComments: leadingComments.length ? leadingComments : undefined
1997
+ };
1998
+ }
1999
+ parsePreviewStatement() {
2000
+ const leadingComments = this.collectLeadingComments();
2001
+ const start = this.currentPos();
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
1141
2012
  };
1142
- this.advance();
1143
- value.range.end = this.currentPos();
1144
2013
  }
1145
- else if (tok.type === TokenType.Parameter) {
1146
- value = {
1147
- kind: 'ParameterRef',
1148
- name: tok.value,
1149
- range: { start: this.currentPos(), end: this.currentPos() }
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
1150
2027
  };
1151
- this.advance();
1152
- value.range.end = this.currentPos();
2028
+ }
2029
+ this.error(`Expected KML or IMPORT CAPSULE after PREVIEW but got '${tok.value}'`, tok);
2030
+ throw new ParseAbort();
2031
+ }
2032
+ // ────────────────────────────────────────────────────────────────────
2033
+ // META — HISTORY / CHANGES / SNAPSHOT
2034
+ // ────────────────────────────────────────────────────────────────────
2035
+ parseHistoryStatement() {
2036
+ const leadingComments = this.collectLeadingComments();
2037
+ const start = this.currentPos();
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';
1153
2050
  }
1154
2051
  else {
1155
- this.error(`Expected string or parameter after CURSOR`, tok);
1156
- value = {
1157
- kind: 'StringLiteral',
1158
- value: '""',
1159
- parsed: '',
1160
- range: { start: this.currentPos(), end: this.currentPos() }
1161
- };
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;
2071
+ return {
2072
+ kind: 'HistoryStatement',
2073
+ target,
2074
+ value,
2075
+ fromSeq,
2076
+ toSeq,
2077
+ limit,
2078
+ cursor,
2079
+ range: { start, end: this.endPos() },
2080
+ leadingComments: leadingComments.length ? leadingComments : undefined
2081
+ };
2082
+ }
2083
+ parseChangesStatement() {
2084
+ const leadingComments = this.collectLeadingComments();
2085
+ const start = 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';
2091
+ }
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';
1162
2096
  }
2097
+ else {
2098
+ this.error(`Expected SINCE or AFTER SEQ after CHANGES but got '${this.current().value}'`, this.current());
2099
+ throw new ParseAbort();
2100
+ }
2101
+ const value = this.parseScalarValue();
2102
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined;
2103
+ return {
2104
+ kind: 'ChangesStatement',
2105
+ mode,
2106
+ value,
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;
1163
2117
  return {
1164
- kind: 'CursorClause',
1165
- value,
1166
- range: { start, end: this.currentPos() }
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
1167
2149
  };
1168
2150
  }
1169
2151
  // ────────────────────────────────────────────────────────────────────
1170
- // Expressions (for FILTER and FIND projections)
2152
+ // Expressions
1171
2153
  // ────────────────────────────────────────────────────────────────────
1172
2154
  parseExpression() {
1173
2155
  return this.parseOrExpression();
@@ -1183,284 +2165,540 @@ class Parser {
1183
2165
  operator: '||',
1184
2166
  left,
1185
2167
  right,
1186
- range: { start, end: right.range.end }
2168
+ range: { start, end: this.endPos() }
1187
2169
  };
1188
2170
  }
1189
2171
  return left;
1190
2172
  }
1191
2173
  parseAndExpression() {
1192
- let left = this.parseComparisonExpression();
2174
+ let left = this.parseEqualityExpression();
1193
2175
  while (this.check(TokenType.And)) {
1194
2176
  const start = left.range.start;
1195
2177
  this.advance();
1196
- const right = this.parseComparisonExpression();
2178
+ const right = this.parseEqualityExpression();
1197
2179
  left = {
1198
2180
  kind: 'BinaryExpression',
1199
2181
  operator: '&&',
1200
2182
  left,
1201
2183
  right,
1202
- range: { start, end: right.range.end }
2184
+ range: { start, end: this.endPos() }
1203
2185
  };
1204
2186
  }
1205
2187
  return left;
1206
2188
  }
1207
- parseComparisonExpression() {
1208
- let left = this.parseUnaryExpression();
1209
- const compOps = [
1210
- TokenType.Eq,
1211
- TokenType.NotEq,
1212
- TokenType.Lt,
1213
- TokenType.Gt,
1214
- TokenType.LtEq,
1215
- TokenType.GtEq
1216
- ];
1217
- if (compOps.includes(this.current().type)) {
2189
+ parseEqualityExpression() {
2190
+ let left = this.parseRelationalExpression();
2191
+ while (this.check(TokenType.Eq) || this.check(TokenType.NotEq)) {
1218
2192
  const start = left.range.start;
1219
- const op = this.current().value;
1220
- this.advance();
1221
- const right = this.parseUnaryExpression();
2193
+ const op = this.advance().type;
2194
+ const right = this.parseRelationalExpression();
1222
2195
  left = {
1223
2196
  kind: 'BinaryExpression',
1224
- operator: op,
2197
+ operator: op === TokenType.Eq ? '==' : '!=',
1225
2198
  left,
1226
2199
  right,
1227
- range: { start, end: right.range.end }
2200
+ range: { start, end: this.endPos() }
1228
2201
  };
1229
2202
  }
1230
2203
  return left;
1231
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;
2249
+ }
1232
2250
  parseUnaryExpression() {
1233
- if (this.check(TokenType.Bang)) {
2251
+ const tok = this.current();
2252
+ if (tok.type === TokenType.Bang || tok.type === TokenType.Minus) {
1234
2253
  const start = this.currentPos();
1235
2254
  this.advance();
1236
- const operand = this.parseUnaryExpression();
2255
+ const operand = this.parsePrimaryExpression();
1237
2256
  return {
1238
2257
  kind: 'UnaryExpression',
1239
- operator: '!',
2258
+ operator: tok.type === TokenType.Bang ? '!' : '-',
1240
2259
  operand,
1241
- range: { start, end: operand.range.end }
2260
+ range: { start, end: this.endPos() }
1242
2261
  };
1243
2262
  }
1244
2263
  return this.parsePrimaryExpression();
1245
2264
  }
1246
2265
  parsePrimaryExpression() {
1247
2266
  const tok = this.current();
1248
- const start = this.currentPos();
1249
- // Function call: NAME(...)
1250
- if (this.isFunctionToken(tok.type)) {
1251
- 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
+ };
1252
2299
  }
1253
- // Variable (may have dot access)
1254
- if (tok.type === TokenType.Variable) {
1255
- const name = tok.value;
1256
- this.advance();
1257
- let expr = {
1258
- kind: 'VariableRef',
1259
- name,
1260
- 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() }
1261
2317
  };
1262
- // Dot access chain
1263
- while (this.check(TokenType.Dot)) {
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)) {
1264
2347
  this.advance();
1265
- const propTok = this.current();
1266
- if (propTok.type === TokenType.Identifier ||
1267
- this.isNonAmbiguousKeyword(propTok.type)) {
1268
- const prop = propTok.value;
1269
- this.advance();
1270
- expr = {
1271
- kind: 'DotExpression',
1272
- object: expr,
1273
- property: prop,
1274
- range: { start, end: this.currentPos() }
1275
- };
1276
- }
1277
- else {
1278
- 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);
1279
2351
  break;
1280
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
+ });
1281
2369
  }
1282
- return expr;
1283
- }
1284
- // Parameter ref
1285
- if (tok.type === TokenType.Parameter) {
1286
- this.advance();
1287
- return {
1288
- kind: 'ParameterRef',
1289
- name: tok.value,
1290
- range: { start, end: this.currentPos() }
1291
- };
1292
2370
  }
1293
- // String literal
1294
- if (tok.type === TokenType.String) {
1295
- this.advance();
1296
- return {
1297
- kind: 'StringLiteral',
1298
- value: tok.value,
1299
- parsed: this.unescapeString(tok.value),
1300
- range: { start, end: this.currentPos() }
1301
- };
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;
1302
2383
  }
1303
- // Number literal
1304
- if (tok.type === TokenType.Number) {
1305
- 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);
1306
2396
  return {
1307
- kind: 'NumberLiteral',
1308
- value: Number(tok.value),
1309
- raw: tok.value,
1310
- range: { start, end: this.currentPos() }
2397
+ kind: 'VariableRef',
2398
+ name: '?unknown',
2399
+ range: { start: this.currentPos(), end: this.endPos() }
1311
2400
  };
1312
2401
  }
1313
- // Boolean
1314
- if (tok.type === TokenType.Boolean) {
1315
- 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);
1316
2424
  return {
1317
- kind: 'BooleanLiteral',
1318
- value: tok.value === 'true',
1319
- range: { start, end: this.currentPos() }
2425
+ kind: 'StringLiteral',
2426
+ value: '""',
2427
+ parsed: '',
2428
+ range: { start: this.currentPos(), end: this.endPos() }
1320
2429
  };
1321
2430
  }
1322
- // Null
1323
- if (tok.type === TokenType.Null) {
1324
- this.advance();
1325
- 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() } };
1326
2473
  }
1327
- // Array
1328
- if (tok.type === TokenType.LBracket) {
1329
- 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();
1330
2480
  }
1331
- // Object
1332
- if (tok.type === TokenType.LBrace) {
1333
- 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();
1334
2486
  }
1335
- // Parenthesized expression
1336
- if (tok.type === TokenType.LParen) {
1337
- this.advance();
1338
- const expr = this.parseExpression();
1339
- this.expect(TokenType.RParen);
1340
- 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();
1341
2508
  }
1342
- // System identifier as literal
1343
- if (tok.type === TokenType.SystemIdent) {
1344
- this.advance();
1345
- return {
1346
- kind: 'StringLiteral',
1347
- value: `"${tok.value}"`,
1348
- parsed: tok.value,
1349
- range: { start, end: this.currentPos() }
1350
- };
2509
+ if (tok.type === TokenType.String) {
2510
+ return this.parseStringLiteral();
1351
2511
  }
1352
- // Identifier (bare word could be used as a key value)
1353
- if (tok.type === TokenType.Identifier) {
1354
- 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);
1355
2543
  return {
1356
- kind: 'StringLiteral',
1357
- value: `"${tok.value}"`,
1358
- parsed: tok.value,
1359
- range: { start, end: this.currentPos() }
2544
+ kind: 'VariableRef',
2545
+ name: '?unknown',
2546
+ range: { start: this.currentPos(), end: this.endPos() }
1360
2547
  };
1361
2548
  }
1362
- this.error(`Unexpected token '${tok.value}' in expression`, tok);
1363
- this.advance();
1364
- return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
2549
+ return this.parseVariableRef();
1365
2550
  }
1366
- parseFunctionCall() {
1367
- const start = this.currentPos();
1368
- const name = this.current().value;
1369
- this.advance();
1370
- this.expect(TokenType.LParen);
1371
- const args = [];
1372
- if (!this.check(TokenType.RParen)) {
1373
- // Handle DISTINCT keyword inside COUNT
1374
- if (this.current().type === TokenType.Distinct) {
1375
- 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);
1376
2573
  this.advance();
1377
- const innerArg = this.parseExpression();
1378
- args.push({
1379
- kind: 'FunctionCallExpr',
1380
- name: 'DISTINCT',
1381
- args: [innerArg],
1382
- range: { start: dStart, end: this.currentPos() }
1383
- });
1384
- }
1385
- else {
1386
- args.push(this.parseExpression());
1387
- }
1388
- while (this.match(TokenType.Comma)) {
1389
- args.push(this.parseExpression());
1390
- }
2574
+ return {
2575
+ kind: 'NullLiteral',
2576
+ range: { start: this.currentPos(), end: this.endPos() }
2577
+ };
1391
2578
  }
1392
- 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);
1393
2587
  return {
1394
- kind: 'FunctionCallExpr',
1395
- name,
1396
- args,
1397
- 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() }
1398
2606
  };
1399
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
+ }
1400
2638
  parseArrayLiteral() {
2639
+ return this.parseArrayWith(() => this.parseExpression());
2640
+ }
2641
+ parseArrayWith(parseElement) {
1401
2642
  const start = this.currentPos();
1402
2643
  this.expect(TokenType.LBracket);
1403
2644
  const elements = [];
1404
- this.skipComments();
2645
+ let trailingComma = false;
1405
2646
  if (!this.check(TokenType.RBracket)) {
1406
- elements.push(this.parseExpression());
1407
- while (this.match(TokenType.Comma)) {
1408
- this.skipComments();
1409
- if (this.check(TokenType.RBracket))
2647
+ do {
2648
+ if (this.check(TokenType.RBracket)) {
2649
+ trailingComma = true;
1410
2650
  break;
1411
- elements.push(this.parseExpression());
1412
- }
2651
+ }
2652
+ elements.push(parseElement());
2653
+ } while (this.match(TokenType.Comma));
1413
2654
  }
1414
- this.skipComments();
1415
2655
  this.expect(TokenType.RBracket);
1416
2656
  return {
1417
2657
  kind: 'ArrayLiteral',
1418
2658
  elements,
1419
- range: { start, end: this.currentPos() }
2659
+ trailingComma: trailingComma || undefined,
2660
+ range: { start, end: this.endPos() }
1420
2661
  };
1421
2662
  }
1422
2663
  parseObjectLiteral() {
1423
2664
  const start = this.currentPos();
1424
2665
  this.expect(TokenType.LBrace);
1425
- const entries = this.parseObjectEntries();
2666
+ const seen = { trailingComma: false };
2667
+ const entries = this.parseEntries(seen, () => this.parseExpression());
1426
2668
  this.expect(TokenType.RBrace);
1427
2669
  return {
1428
2670
  kind: 'ObjectLiteral',
1429
2671
  entries,
1430
- range: { start, end: this.currentPos() }
2672
+ trailingComma: seen.trailingComma || undefined,
2673
+ range: { start, end: this.endPos() }
1431
2674
  };
1432
2675
  }
1433
- parseObjectEntries() {
2676
+ parseEntries(seen, parseValue) {
1434
2677
  const entries = [];
1435
- this.skipComments();
1436
- while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
2678
+ if (this.check(TokenType.RBrace))
2679
+ return entries;
2680
+ do {
1437
2681
  this.skipComments();
1438
- if (this.check(TokenType.RBrace))
2682
+ if (this.check(TokenType.RBrace)) {
2683
+ seen.trailingComma = entries.length > 0;
1439
2684
  break;
1440
- const entryStart = this.currentPos();
2685
+ }
2686
+ const start = this.currentPos();
1441
2687
  const { key, isQuoted } = this.expectKeyWithQuoting();
1442
2688
  this.expectObjectColon(key);
1443
- const value = this.parseExpression();
2689
+ const value = parseValue();
1444
2690
  entries.push({
1445
2691
  kind: 'ObjectEntry',
1446
2692
  key,
1447
2693
  isQuoted,
1448
2694
  value,
1449
- range: { start: entryStart, end: this.currentPos() }
2695
+ range: { start, end: this.endPos() }
1450
2696
  });
1451
- this.skipComments();
1452
- if (this.check(TokenType.RBrace))
1453
- break;
1454
- if (this.match(TokenType.Comma)) {
1455
- this.skipComments();
1456
- continue;
1457
- }
1458
- this.error(`Expected ',' or '}' after object entry`, this.current());
1459
- }
2697
+ } while (this.match(TokenType.Comma));
1460
2698
  return entries;
1461
2699
  }
1462
2700
  // ────────────────────────────────────────────────────────────────────
1463
- // Helpers
2701
+ // Token helpers
1464
2702
  // ────────────────────────────────────────────────────────────────────
1465
2703
  current() {
1466
2704
  return (this.tokens[this.pos] ?? {
@@ -1475,6 +2713,24 @@ class Parser {
1475
2713
  const tok = this.current();
1476
2714
  return { line: tok.line, column: tok.column };
1477
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
+ }
1478
2734
  isAtEnd() {
1479
2735
  return (this.pos >= this.tokens.length || this.current().type === TokenType.EOF);
1480
2736
  }
@@ -1495,6 +2751,10 @@ class Parser {
1495
2751
  this.skipComments();
1496
2752
  return tok;
1497
2753
  }
2754
+ /** The token `i` positions ahead, skipping nothing. */
2755
+ peekPast(i) {
2756
+ return this.tokens[this.pos + i];
2757
+ }
1498
2758
  expect(type) {
1499
2759
  const tok = this.current();
1500
2760
  if (tok.type !== type) {
@@ -1503,41 +2763,55 @@ class Parser {
1503
2763
  }
1504
2764
  return this.advance();
1505
2765
  }
1506
- expectVariable() {
2766
+ /**
2767
+ * Consumes a keyword that the grammar requires to be followed by whitespace.
2768
+ *
2769
+ * Most KIP keywords only need a word boundary, so `WHERE{...}` is legal.
2770
+ * A handful — the statement introducers and the clause keywords whose
2771
+ * operand may itself start with a brace or a quote — require real
2772
+ * whitespace, which is what keeps `MUTATE{` from reading as a statement.
2773
+ * The distinction is per-keyword-position, not per-keyword, so it lives at
2774
+ * the call site rather than in the lexer.
2775
+ */
2776
+ expectKeywordWithSpace(type) {
1507
2777
  const tok = this.current();
1508
- if (tok.type !== TokenType.Variable) {
1509
- this.error(`Expected variable (e.g., ?name) but got '${tok.value}'`, tok);
1510
- return '?unknown';
2778
+ if (tok.type !== type) {
2779
+ this.error(`Expected '${type}' but got '${tok.value}'`, tok);
2780
+ return tok;
1511
2781
  }
1512
- this.advance();
1513
- return tok.value;
1514
- }
1515
- expectString() {
1516
- const tok = this.current();
1517
- if (tok.type !== TokenType.String) {
1518
- this.error(`Expected string literal but got '${tok.value}'`, tok);
1519
- return '';
2782
+ const after = this.source[tok.offset + tok.value.length] ?? '';
2783
+ if (after !== ' ' && after !== '\t' && after !== '\r' && after !== '\n') {
2784
+ this.error(`'${tok.value}' must be followed by whitespace`, tok, 'KIP_1001');
1520
2785
  }
1521
- this.advance();
1522
- return this.unescapeString(tok.value);
2786
+ return this.advance();
1523
2787
  }
1524
- 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) {
1525
2798
  const tok = this.current();
1526
- if (tok.type !== TokenType.String) {
1527
- this.error(`Expected quoted string but got '${tok.value}'`, tok);
1528
- 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);
1529
2802
  }
1530
- this.advance();
1531
- return this.unescapeString(tok.value);
2803
+ return this.expect(type);
1532
2804
  }
1533
2805
  expectKeyWithQuoting() {
1534
2806
  const tok = this.current();
1535
2807
  if (tok.type === TokenType.String) {
1536
2808
  this.advance();
1537
- return { key: this.unescapeString(tok.value), isQuoted: true };
2809
+ return { key: this.unescapeString(tok.value, tok), isQuoted: true };
1538
2810
  }
1539
- if (tok.type === TokenType.Identifier ||
1540
- 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)) {
1541
2815
  this.advance();
1542
2816
  return { key: tok.value, isQuoted: false };
1543
2817
  }
@@ -1545,6 +2819,19 @@ class Parser {
1545
2819
  this.advance();
1546
2820
  return { key: tok.value, isQuoted: false };
1547
2821
  }
2822
+ /** Reports a clause written twice in a statement that allows it once. */
2823
+ rejectRepeat(seen, name, tok) {
2824
+ if (seen !== undefined) {
2825
+ this.error(`Duplicate ${name} clause`, tok);
2826
+ }
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
+ }
1548
2835
  skipComments() {
1549
2836
  while (this.pos < this.tokens.length &&
1550
2837
  this.current().type === TokenType.Comment) {
@@ -1564,73 +2851,66 @@ class Parser {
1564
2851
  /**
1565
2852
  * Consume the `:` separating an object key from its value. A colon written
1566
2853
  * with no space before an identifier value (e.g. `status:active`) is lexed as
1567
- * a single parameter placeholder token (`:active`), so surface a targeted hint
1568
- * instead of the generic "Expected ':'" message.
2854
+ * a single parameter placeholder token (`:active`), so split it back apart.
1569
2855
  */
1570
- expectObjectColon(key) {
2856
+ expectObjectColon(_key) {
1571
2857
  if (this.check(TokenType.Colon)) {
1572
2858
  this.advance();
1573
2859
  return;
1574
2860
  }
2861
+ // `{"a":true}` lexes as a key followed by the parameter `:true`, because
2862
+ // `:name` is the placeholder syntax and the lexer cannot see that this
2863
+ // colon separates a key from its value. In key position the separator
2864
+ // reading is the only valid one, so split the token back apart and re-lex
2865
+ // the tail as the value.
1575
2866
  const tok = this.current();
1576
2867
  if (tok.type === TokenType.Parameter) {
1577
- this.error(`Missing space after ':' — '${key}${tok.value}' was read as a parameter placeholder. ` +
1578
- `Write '${key}: ${tok.value.slice(1)}' (or quote the value).`, tok);
2868
+ this.splitParameterAfterColon(tok);
1579
2869
  return;
1580
2870
  }
1581
2871
  this.expect(TokenType.Colon);
1582
2872
  }
1583
- isFunctionToken(type) {
1584
- return (type === TokenType.Count ||
1585
- type === TokenType.Sum ||
1586
- type === TokenType.Avg ||
1587
- type === TokenType.Min ||
1588
- type === TokenType.Max ||
1589
- type === TokenType.Contains ||
1590
- type === TokenType.StartsWith ||
1591
- type === TokenType.EndsWith ||
1592
- type === TokenType.Regex ||
1593
- type === TokenType.In ||
1594
- type === TokenType.IsNull ||
1595
- type === TokenType.IsNotNull ||
1596
- type === TokenType.Add ||
1597
- type === TokenType.Mul ||
1598
- type === TokenType.Clamp ||
1599
- type === TokenType.Coalesce);
1600
- }
1601
- /** Keywords that can also serve as property names in dot notation or object keys */
1602
- isNonAmbiguousKeyword(type) {
1603
- return (type === TokenType.Type ||
1604
- type === TokenType.Types ||
1605
- type === TokenType.Attributes ||
1606
- type === TokenType.Metadata ||
1607
- type === TokenType.Propositions ||
1608
- type === TokenType.Identifier ||
1609
- // Allow most keywords as property names since KIP uses snake_case for attrs
1610
- type === TokenType.Asc ||
1611
- type === TokenType.Desc ||
1612
- type === TokenType.Primer ||
1613
- type === TokenType.Domains ||
1614
- type === TokenType.From ||
1615
- type === TokenType.By ||
1616
- type === TokenType.Order ||
1617
- type === TokenType.Set ||
1618
- type === TokenType.With ||
1619
- type === TokenType.Into ||
1620
- type === TokenType.Expect ||
1621
- type === TokenType.Version ||
1622
- type === TokenType.Mode ||
1623
- type === TokenType.Threshold);
1624
- }
1625
- unescapeString(raw) {
1626
- if (raw.startsWith('"') && raw.endsWith('"')) {
2873
+ /**
2874
+ * Rewrites a `:value` parameter token in separator position into the value
2875
+ * tokens it spells, so the parser sees `: value`.
2876
+ */
2877
+ splitParameterAfterColon(tok) {
2878
+ const tail = tok.value.slice(1);
2879
+ const retoken = tokenize(tail)
2880
+ .filter((t) => !isTrivia(t.type) && t.type !== TokenType.EOF)
2881
+ .map((t) => ({
2882
+ ...t,
2883
+ offset: tok.offset + 1 + t.offset,
2884
+ line: tok.line,
2885
+ column: tok.column + 1 + t.column
2886
+ }));
2887
+ this.tokens.splice(this.pos, 1, ...retoken);
2888
+ }
2889
+ /**
2890
+ * Reads the value of a string token.
2891
+ *
2892
+ * KIP strings are JSON strings, so `"a\xb"` and an unterminated literal are
2893
+ * both errors but an editor still wants a tree, so the malformed value is
2894
+ * recovered leniently *and* reported. The lenient reading survives into the
2895
+ * tree: `lower` is handed a `Program` and never sees a diagnostic, so a
2896
+ * caller must reject on `severity === 'error'` before lowering, or `"a\xb"`
2897
+ * reaches the engine as `axb`.
2898
+ */
2899
+ unescapeString(raw, tok) {
2900
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
1627
2901
  try {
1628
2902
  return JSON.parse(raw);
1629
2903
  }
1630
2904
  catch {
2905
+ if (tok) {
2906
+ this.error(`Invalid string literal ${raw}: KIP strings are JSON strings`, tok);
2907
+ }
1631
2908
  raw = raw.slice(1, -1);
1632
2909
  }
1633
2910
  }
2911
+ else if (tok) {
2912
+ this.error(`Unterminated string literal ${raw}`, tok);
2913
+ }
1634
2914
  return raw.replace(/\\(.)/g, (_, ch) => {
1635
2915
  switch (ch) {
1636
2916
  case 'n':
@@ -1661,21 +2941,65 @@ class Parser {
1661
2941
  code
1662
2942
  });
1663
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
+ ]);
1664
2973
  recoverToNextStatement() {
1665
- const stmtStarters = new Set([
1666
- TokenType.Find,
1667
- TokenType.Upsert,
1668
- TokenType.Update,
1669
- TokenType.Merge,
1670
- TokenType.Delete,
1671
- TokenType.Describe,
1672
- TokenType.Search,
1673
- TokenType.Export,
1674
- TokenType.EOF
1675
- ]);
1676
- while (!this.isAtEnd() && !stmtStarters.has(this.current().type)) {
2974
+ while (!this.isAtEnd() &&
2975
+ !Parser.STATEMENT_STARTERS.has(this.current().type)) {
1677
2976
  this.pos++;
1678
2977
  }
1679
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
+ }
1680
3004
  }
1681
3005
  //# sourceMappingURL=parser.js.map