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