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