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