@ldclabs/kip-lang 0.1.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/README.md +111 -0
- package/dist/ast.d.ts +216 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/diagnostics.d.ts +14 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +99 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/formatter.d.ts +14 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +678 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +3 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +347 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1359 -0
- package/dist/parser.js.map +1 -0
- package/dist/token.d.ts +104 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +163 -0
- package/dist/token.js.map +1 -0
- package/package.json +37 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
import { tokenize } from './lexer.js';
|
|
2
|
+
import { TokenType, isTrivia } from './token.js';
|
|
3
|
+
export function parse(source) {
|
|
4
|
+
const allTokens = tokenize(source);
|
|
5
|
+
const parser = new Parser(allTokens, source);
|
|
6
|
+
return parser.parse();
|
|
7
|
+
}
|
|
8
|
+
class Parser {
|
|
9
|
+
tokens;
|
|
10
|
+
pos = 0;
|
|
11
|
+
diagnostics = [];
|
|
12
|
+
source;
|
|
13
|
+
constructor(tokens, source) {
|
|
14
|
+
// Filter out trivia for parsing, but keep comments for attachment later
|
|
15
|
+
this.tokens = tokens.filter((t) => !isTrivia(t.type) || t.type === TokenType.Comment);
|
|
16
|
+
this.source = source;
|
|
17
|
+
}
|
|
18
|
+
parse() {
|
|
19
|
+
const statements = [];
|
|
20
|
+
const start = this.currentPos();
|
|
21
|
+
this.skipComments();
|
|
22
|
+
while (!this.isAtEnd()) {
|
|
23
|
+
this.skipComments();
|
|
24
|
+
if (this.isAtEnd())
|
|
25
|
+
break;
|
|
26
|
+
try {
|
|
27
|
+
const stmt = this.parseStatement();
|
|
28
|
+
if (stmt)
|
|
29
|
+
statements.push(stmt);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Error recovery: skip to next statement-level keyword
|
|
33
|
+
this.recoverToNextStatement();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const end = this.currentPos();
|
|
37
|
+
return {
|
|
38
|
+
ast: { kind: 'Program', statements, range: { start, end } },
|
|
39
|
+
diagnostics: this.diagnostics
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Statement dispatch
|
|
44
|
+
// ────────────────────────────────────────────────────────────────────
|
|
45
|
+
parseStatement() {
|
|
46
|
+
const tok = this.current();
|
|
47
|
+
switch (tok.type) {
|
|
48
|
+
case TokenType.Find:
|
|
49
|
+
return this.parseFindStatement();
|
|
50
|
+
case TokenType.Upsert:
|
|
51
|
+
return this.parseUpsertStatement();
|
|
52
|
+
case TokenType.Delete:
|
|
53
|
+
return this.parseDeleteStatement();
|
|
54
|
+
case TokenType.Describe:
|
|
55
|
+
return this.parseDescribeStatement();
|
|
56
|
+
case TokenType.Search:
|
|
57
|
+
return this.parseSearchStatement();
|
|
58
|
+
default:
|
|
59
|
+
this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, DELETE, DESCRIBE, SEARCH)`, tok);
|
|
60
|
+
this.advance();
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ────────────────────────────────────────────────────────────────────
|
|
65
|
+
// FIND
|
|
66
|
+
// ────────────────────────────────────────────────────────────────────
|
|
67
|
+
parseFindStatement() {
|
|
68
|
+
const start = this.currentPos();
|
|
69
|
+
const comments = this.collectLeadingComments();
|
|
70
|
+
this.expect(TokenType.Find);
|
|
71
|
+
this.expect(TokenType.LParen);
|
|
72
|
+
const projections = [];
|
|
73
|
+
if (!this.check(TokenType.RParen)) {
|
|
74
|
+
projections.push(this.parseExpression());
|
|
75
|
+
while (this.match(TokenType.Comma)) {
|
|
76
|
+
projections.push(this.parseExpression());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.expect(TokenType.RParen);
|
|
80
|
+
let where;
|
|
81
|
+
let orderBy;
|
|
82
|
+
let limit;
|
|
83
|
+
let cursor;
|
|
84
|
+
if (this.check(TokenType.Where)) {
|
|
85
|
+
where = this.parseWhereClause();
|
|
86
|
+
}
|
|
87
|
+
if (this.check(TokenType.Order)) {
|
|
88
|
+
orderBy = this.parseOrderBy();
|
|
89
|
+
}
|
|
90
|
+
if (this.check(TokenType.Limit)) {
|
|
91
|
+
limit = this.parseLimitClause();
|
|
92
|
+
}
|
|
93
|
+
if (this.check(TokenType.Cursor)) {
|
|
94
|
+
cursor = this.parseCursorClause();
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
kind: 'FindStatement',
|
|
98
|
+
projections,
|
|
99
|
+
where,
|
|
100
|
+
orderBy,
|
|
101
|
+
limit,
|
|
102
|
+
cursor,
|
|
103
|
+
range: { start, end: this.currentPos() },
|
|
104
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ────────────────────────────────────────────────────────────────────
|
|
108
|
+
// UPSERT
|
|
109
|
+
// ────────────────────────────────────────────────────────────────────
|
|
110
|
+
parseUpsertStatement() {
|
|
111
|
+
const start = this.currentPos();
|
|
112
|
+
const comments = this.collectLeadingComments();
|
|
113
|
+
this.expect(TokenType.Upsert);
|
|
114
|
+
this.expect(TokenType.LBrace);
|
|
115
|
+
const blocks = [];
|
|
116
|
+
this.skipComments();
|
|
117
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
118
|
+
this.skipComments();
|
|
119
|
+
if (this.check(TokenType.Concept)) {
|
|
120
|
+
blocks.push(this.parseConceptBlock());
|
|
121
|
+
}
|
|
122
|
+
else if (this.check(TokenType.Proposition)) {
|
|
123
|
+
blocks.push(this.parsePropositionBlock());
|
|
124
|
+
}
|
|
125
|
+
else if (this.check(TokenType.RBrace)) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.error(`Expected CONCEPT or PROPOSITION inside UPSERT block`, this.current());
|
|
130
|
+
this.advance();
|
|
131
|
+
}
|
|
132
|
+
this.skipComments();
|
|
133
|
+
}
|
|
134
|
+
this.expect(TokenType.RBrace);
|
|
135
|
+
let metadata;
|
|
136
|
+
if (this.check(TokenType.With)) {
|
|
137
|
+
metadata = this.parseWithMetadata();
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
kind: 'UpsertStatement',
|
|
141
|
+
blocks,
|
|
142
|
+
metadata,
|
|
143
|
+
range: { start, end: this.currentPos() },
|
|
144
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
parseConceptBlock() {
|
|
148
|
+
const start = this.currentPos();
|
|
149
|
+
const comments = this.collectLeadingComments();
|
|
150
|
+
this.expect(TokenType.Concept);
|
|
151
|
+
const handle = this.expectVariable();
|
|
152
|
+
this.expect(TokenType.LBrace);
|
|
153
|
+
const matcher = this.parseConceptMatcher();
|
|
154
|
+
let setAttributes;
|
|
155
|
+
let setPropositions;
|
|
156
|
+
let metadata;
|
|
157
|
+
this.skipComments();
|
|
158
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
159
|
+
this.skipComments();
|
|
160
|
+
if (this.check(TokenType.Set)) {
|
|
161
|
+
const setStart = this.currentPos();
|
|
162
|
+
this.advance(); // skip SET
|
|
163
|
+
if (this.check(TokenType.Attributes)) {
|
|
164
|
+
this.advance();
|
|
165
|
+
setAttributes = this.parseSetAttributesBody(setStart);
|
|
166
|
+
}
|
|
167
|
+
else if (this.check(TokenType.Propositions)) {
|
|
168
|
+
this.advance();
|
|
169
|
+
setPropositions = this.parseSetPropositionsBody(setStart);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.error(`Expected ATTRIBUTES or PROPOSITIONS after SET`, this.current());
|
|
173
|
+
this.advance();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (this.check(TokenType.With)) {
|
|
177
|
+
metadata = this.parseWithMetadata();
|
|
178
|
+
}
|
|
179
|
+
else if (this.check(TokenType.RBrace)) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.skipComments();
|
|
184
|
+
if (this.check(TokenType.RBrace))
|
|
185
|
+
break;
|
|
186
|
+
this.error(`Unexpected token '${this.current().value}' in CONCEPT block`, this.current());
|
|
187
|
+
this.advance();
|
|
188
|
+
}
|
|
189
|
+
this.skipComments();
|
|
190
|
+
}
|
|
191
|
+
this.expect(TokenType.RBrace);
|
|
192
|
+
// Concept-level WITH METADATA (outside the CONCEPT braces)
|
|
193
|
+
if (!metadata && this.check(TokenType.With)) {
|
|
194
|
+
metadata = this.parseWithMetadata();
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: 'ConceptBlock',
|
|
198
|
+
handle,
|
|
199
|
+
matcher,
|
|
200
|
+
setAttributes,
|
|
201
|
+
setPropositions,
|
|
202
|
+
metadata,
|
|
203
|
+
range: { start, end: this.currentPos() },
|
|
204
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
parsePropositionBlock() {
|
|
208
|
+
const start = this.currentPos();
|
|
209
|
+
const comments = this.collectLeadingComments();
|
|
210
|
+
this.expect(TokenType.Proposition);
|
|
211
|
+
let handle;
|
|
212
|
+
if (this.check(TokenType.Variable)) {
|
|
213
|
+
handle = this.expectVariable();
|
|
214
|
+
}
|
|
215
|
+
this.expect(TokenType.LBrace);
|
|
216
|
+
// Parse the proposition pattern (subject, "predicate", object)
|
|
217
|
+
this.expect(TokenType.LParen);
|
|
218
|
+
const subject = this.parsePropositionEndpoint();
|
|
219
|
+
this.expect(TokenType.Comma);
|
|
220
|
+
const predicate = this.parsePredicateExpr();
|
|
221
|
+
this.expect(TokenType.Comma);
|
|
222
|
+
const object = this.parsePropositionEndpoint();
|
|
223
|
+
this.expect(TokenType.RParen);
|
|
224
|
+
let setAttributes;
|
|
225
|
+
let metadata;
|
|
226
|
+
this.skipComments();
|
|
227
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
228
|
+
this.skipComments();
|
|
229
|
+
if (this.check(TokenType.Set)) {
|
|
230
|
+
const setStart = this.currentPos();
|
|
231
|
+
this.advance();
|
|
232
|
+
if (this.check(TokenType.Attributes)) {
|
|
233
|
+
this.advance();
|
|
234
|
+
setAttributes = this.parseSetAttributesBody(setStart);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.error(`Expected ATTRIBUTES after SET in PROPOSITION block`, this.current());
|
|
238
|
+
this.advance();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (this.check(TokenType.RBrace)) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.advance();
|
|
246
|
+
}
|
|
247
|
+
this.skipComments();
|
|
248
|
+
}
|
|
249
|
+
this.expect(TokenType.RBrace);
|
|
250
|
+
if (this.check(TokenType.With)) {
|
|
251
|
+
metadata = this.parseWithMetadata();
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
kind: 'PropositionBlock',
|
|
255
|
+
handle,
|
|
256
|
+
subject,
|
|
257
|
+
predicate,
|
|
258
|
+
object,
|
|
259
|
+
setAttributes,
|
|
260
|
+
metadata,
|
|
261
|
+
range: { start, end: this.currentPos() },
|
|
262
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// ────────────────────────────────────────────────────────────────────
|
|
266
|
+
// DELETE
|
|
267
|
+
// ────────────────────────────────────────────────────────────────────
|
|
268
|
+
parseDeleteStatement() {
|
|
269
|
+
const start = this.currentPos();
|
|
270
|
+
const comments = this.collectLeadingComments();
|
|
271
|
+
this.expect(TokenType.Delete);
|
|
272
|
+
let deleteType;
|
|
273
|
+
let keys;
|
|
274
|
+
let target;
|
|
275
|
+
let detach = false;
|
|
276
|
+
if (this.check(TokenType.Attributes)) {
|
|
277
|
+
deleteType = 'ATTRIBUTES';
|
|
278
|
+
this.advance();
|
|
279
|
+
keys = this.parseDeleteKeySet();
|
|
280
|
+
this.expect(TokenType.From);
|
|
281
|
+
target = this.expectVariable();
|
|
282
|
+
}
|
|
283
|
+
else if (this.check(TokenType.Metadata)) {
|
|
284
|
+
deleteType = 'METADATA';
|
|
285
|
+
this.advance();
|
|
286
|
+
keys = this.parseDeleteKeySet();
|
|
287
|
+
this.expect(TokenType.From);
|
|
288
|
+
target = this.expectVariable();
|
|
289
|
+
}
|
|
290
|
+
else if (this.check(TokenType.Propositions)) {
|
|
291
|
+
deleteType = 'PROPOSITIONS';
|
|
292
|
+
this.advance();
|
|
293
|
+
target = this.expectVariable();
|
|
294
|
+
}
|
|
295
|
+
else if (this.check(TokenType.Concept)) {
|
|
296
|
+
deleteType = 'CONCEPT';
|
|
297
|
+
this.advance();
|
|
298
|
+
target = this.expectVariable();
|
|
299
|
+
if (this.check(TokenType.Detach)) {
|
|
300
|
+
detach = true;
|
|
301
|
+
this.advance();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
this.error(`Expected ATTRIBUTES, METADATA, PROPOSITIONS, or CONCEPT after DELETE`, this.current());
|
|
306
|
+
deleteType = 'ATTRIBUTES';
|
|
307
|
+
target = '?unknown';
|
|
308
|
+
}
|
|
309
|
+
const where = this.parseWhereClause();
|
|
310
|
+
return {
|
|
311
|
+
kind: 'DeleteStatement',
|
|
312
|
+
deleteType,
|
|
313
|
+
keys,
|
|
314
|
+
target,
|
|
315
|
+
detach: detach || undefined,
|
|
316
|
+
where,
|
|
317
|
+
range: { start, end: this.currentPos() },
|
|
318
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
parseDeleteKeySet() {
|
|
322
|
+
this.expect(TokenType.LBrace);
|
|
323
|
+
const keys = [];
|
|
324
|
+
if (!this.check(TokenType.RBrace)) {
|
|
325
|
+
keys.push(this.expectString());
|
|
326
|
+
while (this.match(TokenType.Comma)) {
|
|
327
|
+
if (this.check(TokenType.RBrace))
|
|
328
|
+
break;
|
|
329
|
+
keys.push(this.expectString());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
this.expect(TokenType.RBrace);
|
|
333
|
+
return keys;
|
|
334
|
+
}
|
|
335
|
+
// ────────────────────────────────────────────────────────────────────
|
|
336
|
+
// DESCRIBE
|
|
337
|
+
// ────────────────────────────────────────────────────────────────────
|
|
338
|
+
parseDescribeStatement() {
|
|
339
|
+
const start = this.currentPos();
|
|
340
|
+
const comments = this.collectLeadingComments();
|
|
341
|
+
this.expect(TokenType.Describe);
|
|
342
|
+
let describeType;
|
|
343
|
+
let typeName;
|
|
344
|
+
let limit;
|
|
345
|
+
let cursor;
|
|
346
|
+
if (this.check(TokenType.Primer)) {
|
|
347
|
+
describeType = 'PRIMER';
|
|
348
|
+
this.advance();
|
|
349
|
+
}
|
|
350
|
+
else if (this.check(TokenType.Domains)) {
|
|
351
|
+
describeType = 'DOMAINS';
|
|
352
|
+
this.advance();
|
|
353
|
+
}
|
|
354
|
+
else if (this.check(TokenType.Concept)) {
|
|
355
|
+
this.advance();
|
|
356
|
+
if (this.check(TokenType.Types)) {
|
|
357
|
+
describeType = 'CONCEPT_TYPES';
|
|
358
|
+
this.advance();
|
|
359
|
+
}
|
|
360
|
+
else if (this.check(TokenType.Type)) {
|
|
361
|
+
describeType = 'CONCEPT_TYPE';
|
|
362
|
+
this.advance();
|
|
363
|
+
typeName = this.expectStringValue();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
this.error(`Expected TYPE or TYPES after DESCRIBE CONCEPT`, this.current());
|
|
367
|
+
describeType = 'CONCEPT_TYPES';
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (this.check(TokenType.Proposition)) {
|
|
371
|
+
this.advance();
|
|
372
|
+
if (this.check(TokenType.Types)) {
|
|
373
|
+
describeType = 'PROPOSITION_TYPES';
|
|
374
|
+
this.advance();
|
|
375
|
+
}
|
|
376
|
+
else if (this.check(TokenType.Type)) {
|
|
377
|
+
describeType = 'PROPOSITION_TYPE';
|
|
378
|
+
this.advance();
|
|
379
|
+
typeName = this.expectStringValue();
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
this.error(`Expected TYPE or TYPES after DESCRIBE PROPOSITION`, this.current());
|
|
383
|
+
describeType = 'PROPOSITION_TYPES';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
this.error(`Expected PRIMER, DOMAINS, CONCEPT, or PROPOSITION after DESCRIBE`, this.current());
|
|
388
|
+
describeType = 'PRIMER';
|
|
389
|
+
}
|
|
390
|
+
if (this.check(TokenType.Limit)) {
|
|
391
|
+
limit = this.parseLimitClause();
|
|
392
|
+
}
|
|
393
|
+
if (this.check(TokenType.Cursor)) {
|
|
394
|
+
cursor = this.parseCursorClause();
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
kind: 'DescribeStatement',
|
|
398
|
+
describeType,
|
|
399
|
+
typeName,
|
|
400
|
+
limit,
|
|
401
|
+
cursor,
|
|
402
|
+
range: { start, end: this.currentPos() },
|
|
403
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// ────────────────────────────────────────────────────────────────────
|
|
407
|
+
// SEARCH
|
|
408
|
+
// ────────────────────────────────────────────────────────────────────
|
|
409
|
+
parseSearchStatement() {
|
|
410
|
+
const start = this.currentPos();
|
|
411
|
+
const comments = this.collectLeadingComments();
|
|
412
|
+
this.expect(TokenType.Search);
|
|
413
|
+
let searchTarget;
|
|
414
|
+
if (this.check(TokenType.Concept)) {
|
|
415
|
+
searchTarget = 'CONCEPT';
|
|
416
|
+
this.advance();
|
|
417
|
+
}
|
|
418
|
+
else if (this.check(TokenType.Proposition)) {
|
|
419
|
+
searchTarget = 'PROPOSITION';
|
|
420
|
+
this.advance();
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.error(`Expected CONCEPT or PROPOSITION after SEARCH`, this.current());
|
|
424
|
+
searchTarget = 'CONCEPT';
|
|
425
|
+
}
|
|
426
|
+
const term = this.expectStringValue();
|
|
427
|
+
let withType;
|
|
428
|
+
if (this.check(TokenType.With)) {
|
|
429
|
+
this.advance();
|
|
430
|
+
this.expect(TokenType.Type);
|
|
431
|
+
withType = this.expectStringValue();
|
|
432
|
+
}
|
|
433
|
+
let limit;
|
|
434
|
+
if (this.check(TokenType.Limit)) {
|
|
435
|
+
limit = this.parseLimitClause();
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
kind: 'SearchStatement',
|
|
439
|
+
searchTarget,
|
|
440
|
+
term,
|
|
441
|
+
withType,
|
|
442
|
+
limit,
|
|
443
|
+
range: { start, end: this.currentPos() },
|
|
444
|
+
leadingComments: comments.length > 0 ? comments : undefined
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
// ────────────────────────────────────────────────────────────────────
|
|
448
|
+
// WHERE clause and patterns
|
|
449
|
+
// ────────────────────────────────────────────────────────────────────
|
|
450
|
+
parseWhereClause() {
|
|
451
|
+
const start = this.currentPos();
|
|
452
|
+
this.expect(TokenType.Where);
|
|
453
|
+
this.expect(TokenType.LBrace);
|
|
454
|
+
const patterns = this.parseWherePatterns();
|
|
455
|
+
this.expect(TokenType.RBrace);
|
|
456
|
+
return {
|
|
457
|
+
kind: 'WhereClause',
|
|
458
|
+
patterns,
|
|
459
|
+
range: { start, end: this.currentPos() }
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
parseWherePatterns() {
|
|
463
|
+
const patterns = [];
|
|
464
|
+
this.skipComments();
|
|
465
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
466
|
+
this.skipComments();
|
|
467
|
+
if (this.check(TokenType.RBrace))
|
|
468
|
+
break;
|
|
469
|
+
const pattern = this.parseWherePattern();
|
|
470
|
+
if (pattern)
|
|
471
|
+
patterns.push(pattern);
|
|
472
|
+
this.skipComments();
|
|
473
|
+
}
|
|
474
|
+
return patterns;
|
|
475
|
+
}
|
|
476
|
+
parseWherePattern() {
|
|
477
|
+
this.skipComments();
|
|
478
|
+
const tok = this.current();
|
|
479
|
+
if (tok.type === TokenType.Filter) {
|
|
480
|
+
return this.parseFilterClause();
|
|
481
|
+
}
|
|
482
|
+
if (tok.type === TokenType.Not) {
|
|
483
|
+
return this.parseNotClause();
|
|
484
|
+
}
|
|
485
|
+
if (tok.type === TokenType.Optional) {
|
|
486
|
+
return this.parseOptionalClause();
|
|
487
|
+
}
|
|
488
|
+
if (tok.type === TokenType.Union) {
|
|
489
|
+
return this.parseUnionClause();
|
|
490
|
+
}
|
|
491
|
+
// Variable: could be concept pattern or proposition pattern
|
|
492
|
+
if (tok.type === TokenType.Variable) {
|
|
493
|
+
return this.parseVariableLeadingPattern();
|
|
494
|
+
}
|
|
495
|
+
// Opening ( = proposition pattern without variable binding
|
|
496
|
+
if (tok.type === TokenType.LParen) {
|
|
497
|
+
return this.parsePropositionPatternBody(undefined);
|
|
498
|
+
}
|
|
499
|
+
this.error(`Unexpected token '${tok.value}' in WHERE clause`, tok);
|
|
500
|
+
this.advance();
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
parseVariableLeadingPattern() {
|
|
504
|
+
// ?var could be followed by:
|
|
505
|
+
// { ... } => concept pattern
|
|
506
|
+
// ( ... ) => proposition pattern
|
|
507
|
+
const start = this.currentPos();
|
|
508
|
+
const variable = this.expectVariable();
|
|
509
|
+
if (this.check(TokenType.LBrace)) {
|
|
510
|
+
return this.parseConceptPatternBody(variable, start);
|
|
511
|
+
}
|
|
512
|
+
if (this.check(TokenType.LParen)) {
|
|
513
|
+
return this.parsePropositionPatternBody(variable);
|
|
514
|
+
}
|
|
515
|
+
// Just a variable reference as a standalone concept pattern without matcher
|
|
516
|
+
// This occurs in WHERE like: ?drug {type: "Drug"}
|
|
517
|
+
this.error(`Expected '{' or '(' after variable '${variable}' in WHERE clause`, this.current());
|
|
518
|
+
return {
|
|
519
|
+
kind: 'ConceptPattern',
|
|
520
|
+
variable,
|
|
521
|
+
matcher: {
|
|
522
|
+
kind: 'ConceptMatcher',
|
|
523
|
+
entries: [],
|
|
524
|
+
range: { start, end: this.currentPos() }
|
|
525
|
+
},
|
|
526
|
+
range: { start, end: this.currentPos() }
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
parseConceptPatternBody(variable, start) {
|
|
530
|
+
const matcher = this.parseConceptMatcher();
|
|
531
|
+
return {
|
|
532
|
+
kind: 'ConceptPattern',
|
|
533
|
+
variable,
|
|
534
|
+
matcher,
|
|
535
|
+
range: { start, end: this.currentPos() }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
parseConceptMatcher() {
|
|
539
|
+
const start = this.currentPos();
|
|
540
|
+
this.expect(TokenType.LBrace);
|
|
541
|
+
const entries = this.parseObjectEntries();
|
|
542
|
+
this.expect(TokenType.RBrace);
|
|
543
|
+
return {
|
|
544
|
+
kind: 'ConceptMatcher',
|
|
545
|
+
entries,
|
|
546
|
+
range: { start, end: this.currentPos() }
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
parsePropositionPatternBody(variable) {
|
|
550
|
+
const start = this.currentPos();
|
|
551
|
+
this.expect(TokenType.LParen);
|
|
552
|
+
const subject = this.parsePropositionEndpoint();
|
|
553
|
+
this.expect(TokenType.Comma);
|
|
554
|
+
const predicate = this.parsePredicateExpr();
|
|
555
|
+
this.expect(TokenType.Comma);
|
|
556
|
+
const object = this.parsePropositionEndpoint();
|
|
557
|
+
this.expect(TokenType.RParen);
|
|
558
|
+
return {
|
|
559
|
+
kind: 'PropositionPattern',
|
|
560
|
+
variable,
|
|
561
|
+
subject,
|
|
562
|
+
predicate,
|
|
563
|
+
object,
|
|
564
|
+
range: { start, end: this.currentPos() }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
parsePropositionEndpoint() {
|
|
568
|
+
// Could be: ?var, {type: ..., name: ...}, or nested (subject, pred, object)
|
|
569
|
+
if (this.check(TokenType.Variable)) {
|
|
570
|
+
const start = this.currentPos();
|
|
571
|
+
const name = this.expectVariable();
|
|
572
|
+
return {
|
|
573
|
+
kind: 'VariableRef',
|
|
574
|
+
name,
|
|
575
|
+
range: { start, end: this.currentPos() }
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (this.check(TokenType.LBrace)) {
|
|
579
|
+
const start = this.currentPos();
|
|
580
|
+
const matcher = this.parseConceptMatcher();
|
|
581
|
+
return {
|
|
582
|
+
kind: 'ConceptPattern',
|
|
583
|
+
matcher,
|
|
584
|
+
range: { start, end: this.currentPos() }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (this.check(TokenType.LParen)) {
|
|
588
|
+
return this.parsePropositionPatternBody(undefined);
|
|
589
|
+
}
|
|
590
|
+
this.error(`Expected variable, concept pattern, or proposition pattern`, this.current());
|
|
591
|
+
const start = this.currentPos();
|
|
592
|
+
return {
|
|
593
|
+
kind: 'VariableRef',
|
|
594
|
+
name: '?unknown',
|
|
595
|
+
range: { start, end: start }
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
parsePredicateExpr() {
|
|
599
|
+
const start = this.currentPos();
|
|
600
|
+
const first = this.parsePredicateLiteral();
|
|
601
|
+
// Check for alternation: "pred1" | "pred2"
|
|
602
|
+
if (this.check(TokenType.Pipe)) {
|
|
603
|
+
const predicates = [first];
|
|
604
|
+
while (this.match(TokenType.Pipe)) {
|
|
605
|
+
predicates.push(this.parsePredicateLiteral());
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
kind: 'PredicateAlternation',
|
|
609
|
+
predicates,
|
|
610
|
+
range: { start, end: this.currentPos() }
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return first;
|
|
614
|
+
}
|
|
615
|
+
parsePredicateLiteral() {
|
|
616
|
+
const start = this.currentPos();
|
|
617
|
+
const value = this.expectStringValue();
|
|
618
|
+
// Check for hop range: {m,n} {m,} {m}
|
|
619
|
+
let hopRange;
|
|
620
|
+
if (this.check(TokenType.LBrace)) {
|
|
621
|
+
hopRange = this.parseHopRange();
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
kind: 'PredicateLiteral',
|
|
625
|
+
value,
|
|
626
|
+
hopRange,
|
|
627
|
+
range: { start, end: this.currentPos() }
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
parseHopRange() {
|
|
631
|
+
const start = this.currentPos();
|
|
632
|
+
this.expect(TokenType.LBrace);
|
|
633
|
+
const minTok = this.current();
|
|
634
|
+
if (minTok.type !== TokenType.Number) {
|
|
635
|
+
this.error(`Expected number in hop range`, minTok);
|
|
636
|
+
}
|
|
637
|
+
const min = Number(minTok.value);
|
|
638
|
+
this.advance();
|
|
639
|
+
let max;
|
|
640
|
+
if (this.match(TokenType.Comma)) {
|
|
641
|
+
if (this.check(TokenType.Number)) {
|
|
642
|
+
max = Number(this.current().value);
|
|
643
|
+
this.advance();
|
|
644
|
+
}
|
|
645
|
+
// else: {m,} means unbounded
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
max = min; // {m} means exactly m
|
|
649
|
+
}
|
|
650
|
+
this.expect(TokenType.RBrace);
|
|
651
|
+
return {
|
|
652
|
+
kind: 'HopRange',
|
|
653
|
+
min,
|
|
654
|
+
max,
|
|
655
|
+
range: { start, end: this.currentPos() }
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
parseFilterClause() {
|
|
659
|
+
const start = this.currentPos();
|
|
660
|
+
this.expect(TokenType.Filter);
|
|
661
|
+
this.expect(TokenType.LParen);
|
|
662
|
+
const expression = this.parseExpression();
|
|
663
|
+
this.expect(TokenType.RParen);
|
|
664
|
+
return {
|
|
665
|
+
kind: 'FilterClause',
|
|
666
|
+
expression,
|
|
667
|
+
range: { start, end: this.currentPos() }
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
parseNotClause() {
|
|
671
|
+
const start = this.currentPos();
|
|
672
|
+
this.expect(TokenType.Not);
|
|
673
|
+
this.expect(TokenType.LBrace);
|
|
674
|
+
const patterns = this.parseWherePatterns();
|
|
675
|
+
this.expect(TokenType.RBrace);
|
|
676
|
+
return {
|
|
677
|
+
kind: 'NotClause',
|
|
678
|
+
patterns,
|
|
679
|
+
range: { start, end: this.currentPos() }
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
parseOptionalClause() {
|
|
683
|
+
const start = this.currentPos();
|
|
684
|
+
this.expect(TokenType.Optional);
|
|
685
|
+
this.expect(TokenType.LBrace);
|
|
686
|
+
const patterns = this.parseWherePatterns();
|
|
687
|
+
this.expect(TokenType.RBrace);
|
|
688
|
+
return {
|
|
689
|
+
kind: 'OptionalClause',
|
|
690
|
+
patterns,
|
|
691
|
+
range: { start, end: this.currentPos() }
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
parseUnionClause() {
|
|
695
|
+
const start = this.currentPos();
|
|
696
|
+
this.expect(TokenType.Union);
|
|
697
|
+
this.expect(TokenType.LBrace);
|
|
698
|
+
const patterns = this.parseWherePatterns();
|
|
699
|
+
this.expect(TokenType.RBrace);
|
|
700
|
+
return {
|
|
701
|
+
kind: 'UnionClause',
|
|
702
|
+
patterns,
|
|
703
|
+
range: { start, end: this.currentPos() }
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
// ────────────────────────────────────────────────────────────────────
|
|
707
|
+
// SET ATTRIBUTES / SET PROPOSITIONS / WITH METADATA
|
|
708
|
+
// ────────────────────────────────────────────────────────────────────
|
|
709
|
+
parseSetAttributesBody(start) {
|
|
710
|
+
this.expect(TokenType.LBrace);
|
|
711
|
+
const entries = this.parseObjectEntries();
|
|
712
|
+
this.expect(TokenType.RBrace);
|
|
713
|
+
return {
|
|
714
|
+
kind: 'SetAttributes',
|
|
715
|
+
entries,
|
|
716
|
+
range: { start, end: this.currentPos() }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
parseSetPropositionsBody(start) {
|
|
720
|
+
this.expect(TokenType.LBrace);
|
|
721
|
+
const items = [];
|
|
722
|
+
this.skipComments();
|
|
723
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
724
|
+
this.skipComments();
|
|
725
|
+
if (this.check(TokenType.RBrace))
|
|
726
|
+
break;
|
|
727
|
+
items.push(this.parsePropositionItem());
|
|
728
|
+
this.skipComments();
|
|
729
|
+
}
|
|
730
|
+
this.expect(TokenType.RBrace);
|
|
731
|
+
return {
|
|
732
|
+
kind: 'SetPropositions',
|
|
733
|
+
items,
|
|
734
|
+
range: { start, end: this.currentPos() }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
parsePropositionItem() {
|
|
738
|
+
const start = this.currentPos();
|
|
739
|
+
this.expect(TokenType.LParen);
|
|
740
|
+
const predicate = this.expectStringValue();
|
|
741
|
+
this.expect(TokenType.Comma);
|
|
742
|
+
const target = this.parsePropositionEndpoint();
|
|
743
|
+
this.expect(TokenType.RParen);
|
|
744
|
+
let metadata;
|
|
745
|
+
if (this.check(TokenType.With)) {
|
|
746
|
+
metadata = this.parseWithMetadata();
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
kind: 'PropositionItem',
|
|
750
|
+
predicate,
|
|
751
|
+
target,
|
|
752
|
+
metadata,
|
|
753
|
+
range: { start, end: this.currentPos() }
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
parseWithMetadata() {
|
|
757
|
+
const start = this.currentPos();
|
|
758
|
+
this.expect(TokenType.With);
|
|
759
|
+
this.expect(TokenType.Metadata);
|
|
760
|
+
this.expect(TokenType.LBrace);
|
|
761
|
+
const entries = this.parseObjectEntries();
|
|
762
|
+
this.expect(TokenType.RBrace);
|
|
763
|
+
return {
|
|
764
|
+
kind: 'WithMetadata',
|
|
765
|
+
entries,
|
|
766
|
+
range: { start, end: this.currentPos() }
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
// ────────────────────────────────────────────────────────────────────
|
|
770
|
+
// ORDER BY, LIMIT, CURSOR
|
|
771
|
+
// ────────────────────────────────────────────────────────────────────
|
|
772
|
+
parseOrderBy() {
|
|
773
|
+
const start = this.currentPos();
|
|
774
|
+
this.expect(TokenType.Order);
|
|
775
|
+
this.expect(TokenType.By);
|
|
776
|
+
const expression = this.parseExpression();
|
|
777
|
+
let direction = 'ASC';
|
|
778
|
+
if (this.check(TokenType.Asc)) {
|
|
779
|
+
this.advance();
|
|
780
|
+
direction = 'ASC';
|
|
781
|
+
}
|
|
782
|
+
else if (this.check(TokenType.Desc)) {
|
|
783
|
+
this.advance();
|
|
784
|
+
direction = 'DESC';
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
kind: 'OrderByClause',
|
|
788
|
+
expression,
|
|
789
|
+
direction,
|
|
790
|
+
range: { start, end: this.currentPos() }
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
parseLimitClause() {
|
|
794
|
+
const start = this.currentPos();
|
|
795
|
+
this.expect(TokenType.Limit);
|
|
796
|
+
const tok = this.current();
|
|
797
|
+
let value;
|
|
798
|
+
if (tok.type === TokenType.Number) {
|
|
799
|
+
value = {
|
|
800
|
+
kind: 'NumberLiteral',
|
|
801
|
+
value: Number(tok.value),
|
|
802
|
+
raw: tok.value,
|
|
803
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
804
|
+
};
|
|
805
|
+
this.advance();
|
|
806
|
+
value.range.end = this.currentPos();
|
|
807
|
+
}
|
|
808
|
+
else if (tok.type === TokenType.Parameter) {
|
|
809
|
+
value = {
|
|
810
|
+
kind: 'ParameterRef',
|
|
811
|
+
name: tok.value,
|
|
812
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
813
|
+
};
|
|
814
|
+
this.advance();
|
|
815
|
+
value.range.end = this.currentPos();
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
this.error(`Expected number or parameter after LIMIT`, tok);
|
|
819
|
+
value = {
|
|
820
|
+
kind: 'NumberLiteral',
|
|
821
|
+
value: 0,
|
|
822
|
+
raw: '0',
|
|
823
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
kind: 'LimitClause',
|
|
828
|
+
value,
|
|
829
|
+
range: { start, end: this.currentPos() }
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
parseCursorClause() {
|
|
833
|
+
const start = this.currentPos();
|
|
834
|
+
this.expect(TokenType.Cursor);
|
|
835
|
+
const tok = this.current();
|
|
836
|
+
let value;
|
|
837
|
+
if (tok.type === TokenType.String) {
|
|
838
|
+
value = {
|
|
839
|
+
kind: 'StringLiteral',
|
|
840
|
+
value: tok.value,
|
|
841
|
+
parsed: tok.value.slice(1, -1),
|
|
842
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
843
|
+
};
|
|
844
|
+
this.advance();
|
|
845
|
+
value.range.end = this.currentPos();
|
|
846
|
+
}
|
|
847
|
+
else if (tok.type === TokenType.Parameter) {
|
|
848
|
+
value = {
|
|
849
|
+
kind: 'ParameterRef',
|
|
850
|
+
name: tok.value,
|
|
851
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
852
|
+
};
|
|
853
|
+
this.advance();
|
|
854
|
+
value.range.end = this.currentPos();
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
this.error(`Expected string or parameter after CURSOR`, tok);
|
|
858
|
+
value = {
|
|
859
|
+
kind: 'StringLiteral',
|
|
860
|
+
value: '""',
|
|
861
|
+
parsed: '',
|
|
862
|
+
range: { start: this.currentPos(), end: this.currentPos() }
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
kind: 'CursorClause',
|
|
867
|
+
value,
|
|
868
|
+
range: { start, end: this.currentPos() }
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
// ────────────────────────────────────────────────────────────────────
|
|
872
|
+
// Expressions (for FILTER and FIND projections)
|
|
873
|
+
// ────────────────────────────────────────────────────────────────────
|
|
874
|
+
parseExpression() {
|
|
875
|
+
return this.parseOrExpression();
|
|
876
|
+
}
|
|
877
|
+
parseOrExpression() {
|
|
878
|
+
let left = this.parseAndExpression();
|
|
879
|
+
while (this.check(TokenType.Or)) {
|
|
880
|
+
const start = left.range.start;
|
|
881
|
+
this.advance();
|
|
882
|
+
const right = this.parseAndExpression();
|
|
883
|
+
left = {
|
|
884
|
+
kind: 'BinaryExpression',
|
|
885
|
+
operator: '||',
|
|
886
|
+
left,
|
|
887
|
+
right,
|
|
888
|
+
range: { start, end: right.range.end }
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
return left;
|
|
892
|
+
}
|
|
893
|
+
parseAndExpression() {
|
|
894
|
+
let left = this.parseComparisonExpression();
|
|
895
|
+
while (this.check(TokenType.And)) {
|
|
896
|
+
const start = left.range.start;
|
|
897
|
+
this.advance();
|
|
898
|
+
const right = this.parseComparisonExpression();
|
|
899
|
+
left = {
|
|
900
|
+
kind: 'BinaryExpression',
|
|
901
|
+
operator: '&&',
|
|
902
|
+
left,
|
|
903
|
+
right,
|
|
904
|
+
range: { start, end: right.range.end }
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return left;
|
|
908
|
+
}
|
|
909
|
+
parseComparisonExpression() {
|
|
910
|
+
let left = this.parseUnaryExpression();
|
|
911
|
+
const compOps = [
|
|
912
|
+
TokenType.Eq,
|
|
913
|
+
TokenType.NotEq,
|
|
914
|
+
TokenType.Lt,
|
|
915
|
+
TokenType.Gt,
|
|
916
|
+
TokenType.LtEq,
|
|
917
|
+
TokenType.GtEq
|
|
918
|
+
];
|
|
919
|
+
if (compOps.includes(this.current().type)) {
|
|
920
|
+
const start = left.range.start;
|
|
921
|
+
const op = this.current().value;
|
|
922
|
+
this.advance();
|
|
923
|
+
const right = this.parseUnaryExpression();
|
|
924
|
+
left = {
|
|
925
|
+
kind: 'BinaryExpression',
|
|
926
|
+
operator: op,
|
|
927
|
+
left,
|
|
928
|
+
right,
|
|
929
|
+
range: { start, end: right.range.end }
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return left;
|
|
933
|
+
}
|
|
934
|
+
parseUnaryExpression() {
|
|
935
|
+
if (this.check(TokenType.Bang)) {
|
|
936
|
+
const start = this.currentPos();
|
|
937
|
+
this.advance();
|
|
938
|
+
const operand = this.parseUnaryExpression();
|
|
939
|
+
return {
|
|
940
|
+
kind: 'UnaryExpression',
|
|
941
|
+
operator: '!',
|
|
942
|
+
operand,
|
|
943
|
+
range: { start, end: operand.range.end }
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
return this.parsePrimaryExpression();
|
|
947
|
+
}
|
|
948
|
+
parsePrimaryExpression() {
|
|
949
|
+
const tok = this.current();
|
|
950
|
+
const start = this.currentPos();
|
|
951
|
+
// Function call: NAME(...)
|
|
952
|
+
if (this.isFunctionToken(tok.type)) {
|
|
953
|
+
return this.parseFunctionCall();
|
|
954
|
+
}
|
|
955
|
+
// Variable (may have dot access)
|
|
956
|
+
if (tok.type === TokenType.Variable) {
|
|
957
|
+
const name = tok.value;
|
|
958
|
+
this.advance();
|
|
959
|
+
let expr = {
|
|
960
|
+
kind: 'VariableRef',
|
|
961
|
+
name,
|
|
962
|
+
range: { start, end: this.currentPos() }
|
|
963
|
+
};
|
|
964
|
+
// Dot access chain
|
|
965
|
+
while (this.check(TokenType.Dot)) {
|
|
966
|
+
this.advance();
|
|
967
|
+
const propTok = this.current();
|
|
968
|
+
if (propTok.type === TokenType.Identifier ||
|
|
969
|
+
this.isNonAmbiguousKeyword(propTok.type)) {
|
|
970
|
+
const prop = propTok.value;
|
|
971
|
+
this.advance();
|
|
972
|
+
expr = {
|
|
973
|
+
kind: 'DotExpression',
|
|
974
|
+
object: expr,
|
|
975
|
+
property: prop,
|
|
976
|
+
range: { start, end: this.currentPos() }
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
this.error(`Expected property name after '.'`, propTok);
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return expr;
|
|
985
|
+
}
|
|
986
|
+
// Parameter ref
|
|
987
|
+
if (tok.type === TokenType.Parameter) {
|
|
988
|
+
this.advance();
|
|
989
|
+
return {
|
|
990
|
+
kind: 'ParameterRef',
|
|
991
|
+
name: tok.value,
|
|
992
|
+
range: { start, end: this.currentPos() }
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
// String literal
|
|
996
|
+
if (tok.type === TokenType.String) {
|
|
997
|
+
this.advance();
|
|
998
|
+
return {
|
|
999
|
+
kind: 'StringLiteral',
|
|
1000
|
+
value: tok.value,
|
|
1001
|
+
parsed: this.unescapeString(tok.value),
|
|
1002
|
+
range: { start, end: this.currentPos() }
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
// Number literal
|
|
1006
|
+
if (tok.type === TokenType.Number) {
|
|
1007
|
+
this.advance();
|
|
1008
|
+
return {
|
|
1009
|
+
kind: 'NumberLiteral',
|
|
1010
|
+
value: Number(tok.value),
|
|
1011
|
+
raw: tok.value,
|
|
1012
|
+
range: { start, end: this.currentPos() }
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
// Boolean
|
|
1016
|
+
if (tok.type === TokenType.Boolean) {
|
|
1017
|
+
this.advance();
|
|
1018
|
+
return {
|
|
1019
|
+
kind: 'BooleanLiteral',
|
|
1020
|
+
value: tok.value === 'true',
|
|
1021
|
+
range: { start, end: this.currentPos() }
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
// Null
|
|
1025
|
+
if (tok.type === TokenType.Null) {
|
|
1026
|
+
this.advance();
|
|
1027
|
+
return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
|
|
1028
|
+
}
|
|
1029
|
+
// Array
|
|
1030
|
+
if (tok.type === TokenType.LBracket) {
|
|
1031
|
+
return this.parseArrayLiteral();
|
|
1032
|
+
}
|
|
1033
|
+
// Object
|
|
1034
|
+
if (tok.type === TokenType.LBrace) {
|
|
1035
|
+
return this.parseObjectLiteral();
|
|
1036
|
+
}
|
|
1037
|
+
// Parenthesized expression
|
|
1038
|
+
if (tok.type === TokenType.LParen) {
|
|
1039
|
+
this.advance();
|
|
1040
|
+
const expr = this.parseExpression();
|
|
1041
|
+
this.expect(TokenType.RParen);
|
|
1042
|
+
return expr;
|
|
1043
|
+
}
|
|
1044
|
+
// System identifier as literal
|
|
1045
|
+
if (tok.type === TokenType.SystemIdent) {
|
|
1046
|
+
this.advance();
|
|
1047
|
+
return {
|
|
1048
|
+
kind: 'StringLiteral',
|
|
1049
|
+
value: `"${tok.value}"`,
|
|
1050
|
+
parsed: tok.value,
|
|
1051
|
+
range: { start, end: this.currentPos() }
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Identifier (bare word — could be used as a key value)
|
|
1055
|
+
if (tok.type === TokenType.Identifier) {
|
|
1056
|
+
this.advance();
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'StringLiteral',
|
|
1059
|
+
value: `"${tok.value}"`,
|
|
1060
|
+
parsed: tok.value,
|
|
1061
|
+
range: { start, end: this.currentPos() }
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
this.error(`Unexpected token '${tok.value}' in expression`, tok);
|
|
1065
|
+
this.advance();
|
|
1066
|
+
return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
|
|
1067
|
+
}
|
|
1068
|
+
parseFunctionCall() {
|
|
1069
|
+
const start = this.currentPos();
|
|
1070
|
+
const name = this.current().value;
|
|
1071
|
+
this.advance();
|
|
1072
|
+
this.expect(TokenType.LParen);
|
|
1073
|
+
const args = [];
|
|
1074
|
+
if (!this.check(TokenType.RParen)) {
|
|
1075
|
+
// Handle DISTINCT keyword inside COUNT
|
|
1076
|
+
if (this.current().type === TokenType.Distinct) {
|
|
1077
|
+
const dStart = this.currentPos();
|
|
1078
|
+
this.advance();
|
|
1079
|
+
const innerArg = this.parseExpression();
|
|
1080
|
+
args.push({
|
|
1081
|
+
kind: 'FunctionCallExpr',
|
|
1082
|
+
name: 'DISTINCT',
|
|
1083
|
+
args: [innerArg],
|
|
1084
|
+
range: { start: dStart, end: this.currentPos() }
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
args.push(this.parseExpression());
|
|
1089
|
+
}
|
|
1090
|
+
while (this.match(TokenType.Comma)) {
|
|
1091
|
+
args.push(this.parseExpression());
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
this.expect(TokenType.RParen);
|
|
1095
|
+
return {
|
|
1096
|
+
kind: 'FunctionCallExpr',
|
|
1097
|
+
name,
|
|
1098
|
+
args,
|
|
1099
|
+
range: { start, end: this.currentPos() }
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
parseArrayLiteral() {
|
|
1103
|
+
const start = this.currentPos();
|
|
1104
|
+
this.expect(TokenType.LBracket);
|
|
1105
|
+
const elements = [];
|
|
1106
|
+
this.skipComments();
|
|
1107
|
+
if (!this.check(TokenType.RBracket)) {
|
|
1108
|
+
elements.push(this.parseExpression());
|
|
1109
|
+
while (this.match(TokenType.Comma)) {
|
|
1110
|
+
this.skipComments();
|
|
1111
|
+
if (this.check(TokenType.RBracket))
|
|
1112
|
+
break;
|
|
1113
|
+
elements.push(this.parseExpression());
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
this.skipComments();
|
|
1117
|
+
this.expect(TokenType.RBracket);
|
|
1118
|
+
return {
|
|
1119
|
+
kind: 'ArrayLiteral',
|
|
1120
|
+
elements,
|
|
1121
|
+
range: { start, end: this.currentPos() }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
parseObjectLiteral() {
|
|
1125
|
+
const start = this.currentPos();
|
|
1126
|
+
this.expect(TokenType.LBrace);
|
|
1127
|
+
const entries = this.parseObjectEntries();
|
|
1128
|
+
this.expect(TokenType.RBrace);
|
|
1129
|
+
return {
|
|
1130
|
+
kind: 'ObjectLiteral',
|
|
1131
|
+
entries,
|
|
1132
|
+
range: { start, end: this.currentPos() }
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
parseObjectEntries() {
|
|
1136
|
+
const entries = [];
|
|
1137
|
+
this.skipComments();
|
|
1138
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1139
|
+
this.skipComments();
|
|
1140
|
+
if (this.check(TokenType.RBrace))
|
|
1141
|
+
break;
|
|
1142
|
+
const entryStart = this.currentPos();
|
|
1143
|
+
const { key, isQuoted } = this.expectKeyWithQuoting();
|
|
1144
|
+
this.expect(TokenType.Colon);
|
|
1145
|
+
const value = this.parseExpression();
|
|
1146
|
+
entries.push({
|
|
1147
|
+
kind: 'ObjectEntry',
|
|
1148
|
+
key,
|
|
1149
|
+
isQuoted,
|
|
1150
|
+
value,
|
|
1151
|
+
range: { start: entryStart, end: this.currentPos() }
|
|
1152
|
+
});
|
|
1153
|
+
// Optional comma
|
|
1154
|
+
this.match(TokenType.Comma);
|
|
1155
|
+
this.skipComments();
|
|
1156
|
+
}
|
|
1157
|
+
return entries;
|
|
1158
|
+
}
|
|
1159
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1160
|
+
// Helpers
|
|
1161
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1162
|
+
current() {
|
|
1163
|
+
return (this.tokens[this.pos] ?? {
|
|
1164
|
+
type: TokenType.EOF,
|
|
1165
|
+
value: '',
|
|
1166
|
+
offset: this.source.length,
|
|
1167
|
+
line: 0,
|
|
1168
|
+
column: 0
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
currentPos() {
|
|
1172
|
+
const tok = this.current();
|
|
1173
|
+
return { line: tok.line, column: tok.column };
|
|
1174
|
+
}
|
|
1175
|
+
isAtEnd() {
|
|
1176
|
+
return (this.pos >= this.tokens.length || this.current().type === TokenType.EOF);
|
|
1177
|
+
}
|
|
1178
|
+
check(type) {
|
|
1179
|
+
return this.current().type === type;
|
|
1180
|
+
}
|
|
1181
|
+
match(type) {
|
|
1182
|
+
if (this.check(type)) {
|
|
1183
|
+
this.advance();
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
advance() {
|
|
1189
|
+
const tok = this.current();
|
|
1190
|
+
if (!this.isAtEnd())
|
|
1191
|
+
this.pos++;
|
|
1192
|
+
this.skipComments();
|
|
1193
|
+
return tok;
|
|
1194
|
+
}
|
|
1195
|
+
expect(type) {
|
|
1196
|
+
const tok = this.current();
|
|
1197
|
+
if (tok.type !== type) {
|
|
1198
|
+
this.error(`Expected '${type}' but got '${tok.value}'`, tok);
|
|
1199
|
+
return tok;
|
|
1200
|
+
}
|
|
1201
|
+
return this.advance();
|
|
1202
|
+
}
|
|
1203
|
+
expectVariable() {
|
|
1204
|
+
const tok = this.current();
|
|
1205
|
+
if (tok.type !== TokenType.Variable) {
|
|
1206
|
+
this.error(`Expected variable (e.g., ?name) but got '${tok.value}'`, tok);
|
|
1207
|
+
return '?unknown';
|
|
1208
|
+
}
|
|
1209
|
+
this.advance();
|
|
1210
|
+
return tok.value;
|
|
1211
|
+
}
|
|
1212
|
+
expectString() {
|
|
1213
|
+
const tok = this.current();
|
|
1214
|
+
if (tok.type !== TokenType.String) {
|
|
1215
|
+
this.error(`Expected string literal but got '${tok.value}'`, tok);
|
|
1216
|
+
return '';
|
|
1217
|
+
}
|
|
1218
|
+
this.advance();
|
|
1219
|
+
return this.unescapeString(tok.value);
|
|
1220
|
+
}
|
|
1221
|
+
expectStringValue() {
|
|
1222
|
+
const tok = this.current();
|
|
1223
|
+
if (tok.type !== TokenType.String) {
|
|
1224
|
+
this.error(`Expected quoted string but got '${tok.value}'`, tok);
|
|
1225
|
+
return '';
|
|
1226
|
+
}
|
|
1227
|
+
this.advance();
|
|
1228
|
+
return this.unescapeString(tok.value);
|
|
1229
|
+
}
|
|
1230
|
+
expectKey() {
|
|
1231
|
+
const tok = this.current();
|
|
1232
|
+
// Keys can be identifiers, strings, or even some keywords used as keys
|
|
1233
|
+
if (tok.type === TokenType.String) {
|
|
1234
|
+
this.advance();
|
|
1235
|
+
return this.unescapeString(tok.value);
|
|
1236
|
+
}
|
|
1237
|
+
if (tok.type === TokenType.Identifier ||
|
|
1238
|
+
this.isNonAmbiguousKeyword(tok.type)) {
|
|
1239
|
+
this.advance();
|
|
1240
|
+
return tok.value;
|
|
1241
|
+
}
|
|
1242
|
+
this.error(`Expected object key but got '${tok.value}'`, tok);
|
|
1243
|
+
this.advance();
|
|
1244
|
+
return tok.value;
|
|
1245
|
+
}
|
|
1246
|
+
expectKeyWithQuoting() {
|
|
1247
|
+
const tok = this.current();
|
|
1248
|
+
if (tok.type === TokenType.String) {
|
|
1249
|
+
this.advance();
|
|
1250
|
+
return { key: this.unescapeString(tok.value), isQuoted: true };
|
|
1251
|
+
}
|
|
1252
|
+
if (tok.type === TokenType.Identifier ||
|
|
1253
|
+
this.isNonAmbiguousKeyword(tok.type)) {
|
|
1254
|
+
this.advance();
|
|
1255
|
+
return { key: tok.value, isQuoted: false };
|
|
1256
|
+
}
|
|
1257
|
+
this.error(`Expected object key but got '${tok.value}'`, tok);
|
|
1258
|
+
this.advance();
|
|
1259
|
+
return { key: tok.value, isQuoted: false };
|
|
1260
|
+
}
|
|
1261
|
+
skipComments() {
|
|
1262
|
+
while (this.pos < this.tokens.length &&
|
|
1263
|
+
this.current().type === TokenType.Comment) {
|
|
1264
|
+
this.pos++;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
collectLeadingComments() {
|
|
1268
|
+
const comments = [];
|
|
1269
|
+
// Look backwards from current position to collect contiguous comment tokens
|
|
1270
|
+
let i = this.pos - 1;
|
|
1271
|
+
while (i >= 0 && this.tokens[i].type === TokenType.Comment) {
|
|
1272
|
+
comments.unshift(this.tokens[i].value);
|
|
1273
|
+
i--;
|
|
1274
|
+
}
|
|
1275
|
+
return comments;
|
|
1276
|
+
}
|
|
1277
|
+
isFunctionToken(type) {
|
|
1278
|
+
return (type === TokenType.Count ||
|
|
1279
|
+
type === TokenType.Sum ||
|
|
1280
|
+
type === TokenType.Avg ||
|
|
1281
|
+
type === TokenType.Min ||
|
|
1282
|
+
type === TokenType.Max ||
|
|
1283
|
+
type === TokenType.Contains ||
|
|
1284
|
+
type === TokenType.StartsWith ||
|
|
1285
|
+
type === TokenType.EndsWith ||
|
|
1286
|
+
type === TokenType.Regex ||
|
|
1287
|
+
type === TokenType.In ||
|
|
1288
|
+
type === TokenType.IsNull ||
|
|
1289
|
+
type === TokenType.IsNotNull);
|
|
1290
|
+
}
|
|
1291
|
+
/** Keywords that can also serve as property names in dot notation or object keys */
|
|
1292
|
+
isNonAmbiguousKeyword(type) {
|
|
1293
|
+
return (type === TokenType.Type ||
|
|
1294
|
+
type === TokenType.Types ||
|
|
1295
|
+
type === TokenType.Attributes ||
|
|
1296
|
+
type === TokenType.Metadata ||
|
|
1297
|
+
type === TokenType.Propositions ||
|
|
1298
|
+
type === TokenType.Identifier ||
|
|
1299
|
+
// Allow most keywords as property names since KIP uses snake_case for attrs
|
|
1300
|
+
type === TokenType.Asc ||
|
|
1301
|
+
type === TokenType.Desc ||
|
|
1302
|
+
type === TokenType.Primer ||
|
|
1303
|
+
type === TokenType.Domains ||
|
|
1304
|
+
type === TokenType.From ||
|
|
1305
|
+
type === TokenType.By ||
|
|
1306
|
+
type === TokenType.Order ||
|
|
1307
|
+
type === TokenType.Set ||
|
|
1308
|
+
type === TokenType.With);
|
|
1309
|
+
}
|
|
1310
|
+
unescapeString(raw) {
|
|
1311
|
+
// Strip quotes
|
|
1312
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
1313
|
+
raw = raw.slice(1, -1);
|
|
1314
|
+
}
|
|
1315
|
+
return raw.replace(/\\(.)/g, (_, ch) => {
|
|
1316
|
+
switch (ch) {
|
|
1317
|
+
case 'n':
|
|
1318
|
+
return '\n';
|
|
1319
|
+
case 't':
|
|
1320
|
+
return '\t';
|
|
1321
|
+
case 'r':
|
|
1322
|
+
return '\r';
|
|
1323
|
+
case '\\':
|
|
1324
|
+
return '\\';
|
|
1325
|
+
case '"':
|
|
1326
|
+
return '"';
|
|
1327
|
+
case '/':
|
|
1328
|
+
return '/';
|
|
1329
|
+
default:
|
|
1330
|
+
return ch;
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
error(message, token) {
|
|
1335
|
+
this.diagnostics.push({
|
|
1336
|
+
range: {
|
|
1337
|
+
start: { line: token.line, column: token.column },
|
|
1338
|
+
end: { line: token.line, column: token.column + token.value.length }
|
|
1339
|
+
},
|
|
1340
|
+
severity: 'error',
|
|
1341
|
+
message,
|
|
1342
|
+
code: 'KIP_PARSE'
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
recoverToNextStatement() {
|
|
1346
|
+
const stmtStarters = new Set([
|
|
1347
|
+
TokenType.Find,
|
|
1348
|
+
TokenType.Upsert,
|
|
1349
|
+
TokenType.Delete,
|
|
1350
|
+
TokenType.Describe,
|
|
1351
|
+
TokenType.Search,
|
|
1352
|
+
TokenType.EOF
|
|
1353
|
+
]);
|
|
1354
|
+
while (!this.isAtEnd() && !stmtStarters.has(this.current().type)) {
|
|
1355
|
+
this.pos++;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
//# sourceMappingURL=parser.js.map
|