@ldclabs/kip-lang 0.4.0 → 2.0.1
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 +1355 -594
- 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 +2094 -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/lower.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { invalidSyntax } from './errors.js';
|
|
2
|
-
/**
|
|
3
|
-
* `LIMIT` is a `usize` in the reference grammar, which is 32-bit on the
|
|
4
|
-
* WebAssembly target every JavaScript KIP engine is compiled for.
|
|
5
|
-
*/
|
|
6
|
-
const MAX_LIMIT = 0xffffffff;
|
|
7
|
-
/** A KIP integer is whatever an i64 or a u64 can hold, so the union of both. */
|
|
8
|
-
const I64_MIN = -(2n ** 63n);
|
|
9
|
-
const U64_MAX = 2n ** 64n - 1n;
|
|
10
2
|
const AGGREGATIONS = new Map([
|
|
11
3
|
['COUNT', 'Count'],
|
|
12
4
|
['SUM', 'Sum'],
|
|
@@ -21,7 +13,11 @@ const FILTER_FUNCTIONS = new Map([
|
|
|
21
13
|
['REGEX', 'Regex'],
|
|
22
14
|
['IN', 'In'],
|
|
23
15
|
['IS_NULL', 'IsNull'],
|
|
24
|
-
['IS_NOT_NULL', 'IsNotNull']
|
|
16
|
+
['IS_NOT_NULL', 'IsNotNull'],
|
|
17
|
+
['IS_LITERAL', 'IsLiteral'],
|
|
18
|
+
['IS_ELEMENT', 'IsElement'],
|
|
19
|
+
['IS_KIND', 'IsKind'],
|
|
20
|
+
['LITERAL_TYPE', 'LiteralType']
|
|
25
21
|
]);
|
|
26
22
|
const UPDATE_FUNCTIONS = new Map([
|
|
27
23
|
['ADD', 'Add'],
|
|
@@ -43,10 +39,47 @@ const COMPARISONS = new Map([
|
|
|
43
39
|
['<=', 'LessEqual'],
|
|
44
40
|
['>=', 'GreaterEqual']
|
|
45
41
|
]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Engine-owned state no cognitive mutation may write (Spec §6.3, §2.11).
|
|
44
|
+
*
|
|
45
|
+
* These are checked by name on every mutation, not just on UPDATE: author
|
|
46
|
+
* content that could rewrite engine truth or its own authority is exactly
|
|
47
|
+
* what "external cognition cannot self-escalate authority" forbids.
|
|
48
|
+
*/
|
|
49
|
+
const PROTECTED_FIELDS = new Set([
|
|
50
|
+
'_system',
|
|
51
|
+
'governance',
|
|
52
|
+
'space_id',
|
|
53
|
+
'space_seq'
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* Assertion payload that is immutable after creation (Spec §13.7).
|
|
57
|
+
*
|
|
58
|
+
* Changing epistemic commitment means a new Assertion plus supersession, so
|
|
59
|
+
* an UPDATE naming one of these is the `EpistemicRevisionRequired` mistake
|
|
60
|
+
* caught statically wherever the WHERE block says the target is an Assertion.
|
|
61
|
+
*/
|
|
62
|
+
const ASSERTION_IMMUTABLE = new Set([
|
|
63
|
+
'proposition_id',
|
|
64
|
+
'proposition',
|
|
65
|
+
'asserted_by',
|
|
66
|
+
'stance',
|
|
67
|
+
'mode',
|
|
68
|
+
'confidence',
|
|
69
|
+
'asserted_at',
|
|
70
|
+
'valid_time',
|
|
71
|
+
'evidence_refs'
|
|
72
|
+
]);
|
|
73
|
+
/** Evidence payload and observation identity are immutable (Spec §15.5). */
|
|
74
|
+
const EVIDENCE_IMMUTABLE = new Set([
|
|
75
|
+
'evidence_class',
|
|
76
|
+
'payload',
|
|
77
|
+
'content_digest',
|
|
78
|
+
'media_type',
|
|
79
|
+
'observed_at'
|
|
80
|
+
]);
|
|
81
|
+
/** A Proposition tuple is immutable after creation (Spec §12.5). */
|
|
82
|
+
const PROPOSITION_IMMUTABLE = new Set(['subject', 'predicate', 'object']);
|
|
50
83
|
/**
|
|
51
84
|
* Lowers a parsed program carrying exactly one command.
|
|
52
85
|
*
|
|
@@ -62,280 +95,352 @@ export function lower(program) {
|
|
|
62
95
|
if (!first) {
|
|
63
96
|
throw invalidSyntax('expected a KIP command, found none');
|
|
64
97
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// is written as several `UPSERT { ... }` in sequence and applies as a unit.
|
|
69
|
-
// Anything else following a complete command is trailing content.
|
|
70
|
-
if (program.statements.every((s) => s.kind === 'UpsertStatement')) {
|
|
71
|
-
return {
|
|
72
|
-
Kml: {
|
|
73
|
-
Upsert: program.statements.map(lowerUpsert)
|
|
74
|
-
}
|
|
75
|
-
};
|
|
98
|
+
if (second) {
|
|
99
|
+
throw invalidSyntax('expected one KIP command, found several: wrap consecutive mutations in ' +
|
|
100
|
+
'MUTATE { ... } to make them one transaction, or use lowerAll for a batch', second.range);
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
return lowerStatement(first);
|
|
78
103
|
}
|
|
79
|
-
/** Lowers every
|
|
104
|
+
/** Lowers every statement in a multi-command source text. */
|
|
80
105
|
export function lowerAll(program) {
|
|
106
|
+
if (program.statements.length === 0) {
|
|
107
|
+
throw invalidSyntax('expected at least one KIP command, found none');
|
|
108
|
+
}
|
|
81
109
|
return program.statements.map(lowerStatement);
|
|
82
110
|
}
|
|
83
|
-
/** Lowers one statement. */
|
|
84
111
|
export function lowerStatement(stmt) {
|
|
85
112
|
switch (stmt.kind) {
|
|
86
113
|
case 'FindStatement':
|
|
87
114
|
return { Kql: lowerFind(stmt) };
|
|
88
|
-
case '
|
|
89
|
-
return { Kml:
|
|
115
|
+
case 'MutateStatement':
|
|
116
|
+
return { Kml: lowerMutate(stmt) };
|
|
117
|
+
case 'CreateConceptStatement':
|
|
118
|
+
case 'UpsertConceptStatement':
|
|
119
|
+
case 'EnsurePropositionStatement':
|
|
120
|
+
case 'AssertStatement':
|
|
121
|
+
case 'CreateEvidenceStatement':
|
|
122
|
+
case 'CreateAssertionStatement':
|
|
123
|
+
case 'CreateActivityStatement':
|
|
90
124
|
case 'UpdateStatement':
|
|
91
|
-
|
|
92
|
-
case '
|
|
93
|
-
|
|
94
|
-
case '
|
|
95
|
-
|
|
96
|
-
case '
|
|
97
|
-
|
|
98
|
-
case '
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
case 'RetractAssertionStatement':
|
|
126
|
+
case 'SupersedeAssertionStatement':
|
|
127
|
+
case 'CorrectEvidenceStatement':
|
|
128
|
+
case 'TransitionActivityStatement':
|
|
129
|
+
case 'SetRetentionStatement':
|
|
130
|
+
case 'ArchiveStatement':
|
|
131
|
+
case 'TombstoneStatement':
|
|
132
|
+
case 'PurgeStatement':
|
|
133
|
+
case 'MergeConceptStatement':
|
|
134
|
+
const clauses = lowerMutationClause(stmt, 0);
|
|
135
|
+
assertUniqueHandles(clauses, stmt.range);
|
|
136
|
+
assertResolvedHandles(clauses, stmt.range);
|
|
137
|
+
return {
|
|
138
|
+
Kml: {
|
|
139
|
+
explicit_transaction: false,
|
|
140
|
+
clauses
|
|
141
|
+
}
|
|
105
142
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
exportCmd.cursor = cursor;
|
|
109
|
-
return { Meta: { Export: exportCmd } };
|
|
110
|
-
}
|
|
143
|
+
default:
|
|
144
|
+
return { Meta: lowerMeta(stmt) };
|
|
111
145
|
}
|
|
112
146
|
}
|
|
113
147
|
// ---------------------------------------------------------------------------
|
|
114
148
|
// KQL
|
|
115
149
|
// ---------------------------------------------------------------------------
|
|
116
150
|
function lowerFind(stmt) {
|
|
117
|
-
if (
|
|
118
|
-
throw invalidSyntax('FIND requires
|
|
151
|
+
if (stmt.projections.length === 0) {
|
|
152
|
+
throw invalidSyntax('FIND requires at least one projection', stmt.range);
|
|
119
153
|
}
|
|
120
154
|
return {
|
|
121
|
-
find_clause: {
|
|
155
|
+
find_clause: {
|
|
156
|
+
expressions: stmt.projections.map((p) => lowerFindExpression(p))
|
|
157
|
+
},
|
|
122
158
|
where_clauses: lowerWhere(stmt.where),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null,
|
|
160
|
+
for_time: stmt.forTime ? lowerScalar(stmt.forTime.value) : null,
|
|
161
|
+
epistemic: stmt.epistemic ? lowerBoundObject(stmt.epistemic.options) : null,
|
|
162
|
+
order_by: stmt.orderBy ? stmt.orderBy.items.map(lowerOrderItem) : null,
|
|
163
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
164
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
126
165
|
};
|
|
127
166
|
}
|
|
167
|
+
function lowerAsOf(clause) {
|
|
168
|
+
const value = lowerScalar(clause.value);
|
|
169
|
+
switch (clause.basis) {
|
|
170
|
+
case 'SEQ':
|
|
171
|
+
return { Seq: value };
|
|
172
|
+
case 'TX':
|
|
173
|
+
return { Tx: value };
|
|
174
|
+
case 'TIME':
|
|
175
|
+
return { Time: value };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
128
178
|
function lowerFindExpression(expr) {
|
|
129
|
-
if (expr.kind === '
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return { Variable: dotPathVar(expr) };
|
|
134
|
-
}
|
|
135
|
-
function lowerOrderByKey(key) {
|
|
136
|
-
const direction = key.direction === 'DESC' ? 'Desc' : 'Asc';
|
|
137
|
-
if (key.expression.kind === 'FunctionCallExpr') {
|
|
138
|
-
const { func, arg, distinct } = aggregation(key.expression);
|
|
139
|
-
// A sort key carries no `distinct` flag, so accepting the modifier would
|
|
140
|
-
// sort by a different aggregate than the one projected.
|
|
141
|
-
if (distinct) {
|
|
142
|
-
throw invalidSyntax('ORDER BY takes no DISTINCT: sort by the projected aggregate instead', key.expression.range);
|
|
179
|
+
if (expr.kind === 'AggregateExpr') {
|
|
180
|
+
const func = AGGREGATIONS.get(expr.name.toUpperCase());
|
|
181
|
+
if (!func) {
|
|
182
|
+
throw invalidSyntax(`unknown aggregate ${expr.name}`, expr.range);
|
|
143
183
|
}
|
|
144
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
Aggregation: {
|
|
186
|
+
func,
|
|
187
|
+
var: lowerDotPath(expr.argument),
|
|
188
|
+
distinct: expr.distinct
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { Variable: lowerDotPath(expr) };
|
|
193
|
+
}
|
|
194
|
+
function lowerOrderItem(item) {
|
|
195
|
+
const direction = item.direction === 'DESC' ? 'Desc' : 'Asc';
|
|
196
|
+
if (item.expression.kind === 'AggregateExpr') {
|
|
197
|
+
const func = AGGREGATIONS.get(item.expression.name.toUpperCase());
|
|
198
|
+
if (!func) {
|
|
199
|
+
throw invalidSyntax(`unknown aggregate ${item.expression.name}`, item.expression.range);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
variable: lowerDotPath(item.expression.argument),
|
|
203
|
+
direction,
|
|
204
|
+
aggregation: func
|
|
205
|
+
};
|
|
145
206
|
}
|
|
146
207
|
return {
|
|
147
|
-
variable:
|
|
208
|
+
variable: lowerDotPath(item.expression),
|
|
148
209
|
direction,
|
|
149
210
|
aggregation: null
|
|
150
211
|
};
|
|
151
212
|
}
|
|
152
|
-
/**
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let args = expr.args;
|
|
157
|
-
let distinct = false;
|
|
158
|
-
const [head] = args;
|
|
159
|
-
if (args.length === 1 &&
|
|
160
|
-
head &&
|
|
161
|
-
head.kind === 'FunctionCallExpr' &&
|
|
162
|
-
head.name === 'DISTINCT') {
|
|
163
|
-
distinct = true;
|
|
164
|
-
args = head.args;
|
|
165
|
-
}
|
|
166
|
-
const func = AGGREGATIONS.get(expr.name);
|
|
167
|
-
if (!func) {
|
|
168
|
-
throw invalidSyntax(`unknown aggregation function ${expr.name}(...): expected COUNT, SUM, AVG, MIN or MAX`, expr.range);
|
|
169
|
-
}
|
|
170
|
-
if (args.length !== 1) {
|
|
171
|
-
throw invalidSyntax(`${expr.name} takes exactly one argument, got ${args.length}`, expr.range);
|
|
172
|
-
}
|
|
173
|
-
return { func, arg: dotPathVar(args[0]), distinct };
|
|
174
|
-
}
|
|
175
|
-
function lowerWhere(where) {
|
|
176
|
-
const clauses = where.patterns.map(lowerWherePattern);
|
|
177
|
-
if (clauses.length === 0) {
|
|
178
|
-
throw invalidSyntax('WHERE must contain at least one clause', where.range);
|
|
213
|
+
/** A projection or sort key must resolve to one variable plus a path. */
|
|
214
|
+
function lowerDotPath(expr) {
|
|
215
|
+
if (expr.kind === 'VariableRef') {
|
|
216
|
+
return { var: varName(expr.name, expr.range), path: [] };
|
|
179
217
|
}
|
|
180
|
-
|
|
218
|
+
if (expr.kind === 'FieldAccess') {
|
|
219
|
+
const path = expr.steps.map((step) => step.kind === 'DotStep'
|
|
220
|
+
? { Field: step.name }
|
|
221
|
+
: { Key: step.key.parsed });
|
|
222
|
+
return { var: varName(expr.base.name, expr.base.range), path };
|
|
223
|
+
}
|
|
224
|
+
throw invalidSyntax(`expected a variable or a dot path, found ${describeExpression(expr)}`, expr.range);
|
|
225
|
+
}
|
|
226
|
+
function lowerWhere(clause) {
|
|
227
|
+
return clause.patterns.map(lowerWherePattern);
|
|
181
228
|
}
|
|
182
229
|
function lowerWherePattern(pattern) {
|
|
183
230
|
switch (pattern.kind) {
|
|
184
|
-
case 'ConceptPattern':
|
|
185
|
-
if (!pattern.variable) {
|
|
186
|
-
throw invalidSyntax('a concept clause in WHERE must bind a variable, e.g. ?x {type: "T"}', pattern.range);
|
|
187
|
-
}
|
|
231
|
+
case 'ConceptPattern':
|
|
188
232
|
return {
|
|
189
233
|
Concept: {
|
|
190
|
-
variable: varName(pattern.variable, pattern.range),
|
|
191
|
-
matcher:
|
|
234
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
235
|
+
matcher: lowerObjectMatcher(pattern.matcher)
|
|
192
236
|
}
|
|
193
237
|
};
|
|
194
|
-
}
|
|
195
238
|
case 'PropositionPattern':
|
|
196
239
|
return {
|
|
197
240
|
Proposition: {
|
|
198
241
|
variable: pattern.variable
|
|
199
|
-
? varName(pattern.variable, pattern.range)
|
|
242
|
+
? varName(pattern.variable.name, pattern.variable.range)
|
|
200
243
|
: null,
|
|
201
|
-
matcher: lowerPropositionMatcher(pattern)
|
|
244
|
+
matcher: lowerPropositionMatcher(pattern.tuple)
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
case 'AssertionPattern':
|
|
248
|
+
return {
|
|
249
|
+
Assertion: {
|
|
250
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
251
|
+
matcher: lowerObjectMatcher(pattern.matcher)
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
case 'EvidencePattern':
|
|
255
|
+
return {
|
|
256
|
+
Evidence: {
|
|
257
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
258
|
+
matcher: lowerObjectMatcher(pattern.matcher)
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
case 'ActivityPattern':
|
|
262
|
+
return {
|
|
263
|
+
Activity: {
|
|
264
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
265
|
+
matcher: lowerObjectMatcher(pattern.matcher)
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
case 'StructuralPattern':
|
|
269
|
+
return {
|
|
270
|
+
Structural: {
|
|
271
|
+
variable: pattern.variable
|
|
272
|
+
? varName(pattern.variable.name, pattern.variable.range)
|
|
273
|
+
: null,
|
|
274
|
+
subject: lowerTerm(pattern.subject),
|
|
275
|
+
field: lowerSymbol(pattern.field),
|
|
276
|
+
object: lowerTerm(pattern.object)
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
case 'BeliefPattern': {
|
|
280
|
+
let target;
|
|
281
|
+
if (pattern.proposition) {
|
|
282
|
+
target = {
|
|
283
|
+
Proposition: varName(pattern.proposition.name, pattern.proposition.range)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
else if (pattern.propositionId) {
|
|
287
|
+
// Same slot, same reference form as `?p PROPOSITION (id: ...)`.
|
|
288
|
+
target = { Id: lowerScalar(pattern.propositionId) };
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
if (!pattern.subject || !pattern.predicate || !pattern.object) {
|
|
292
|
+
throw invalidSyntax('BELIEF requires one bound Proposition, an (id: ...) reference, or a full (subject, predicate, object) tuple', pattern.range);
|
|
293
|
+
}
|
|
294
|
+
target = {
|
|
295
|
+
Tuple: {
|
|
296
|
+
subject: lowerPropositionSubject(pattern.subject),
|
|
297
|
+
predicate: { Atom: lowerPredAtom(pattern.predicate) },
|
|
298
|
+
object: lowerTerm(pattern.object)
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
Belief: {
|
|
304
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
305
|
+
target
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case 'BeliefSlotPattern':
|
|
310
|
+
return {
|
|
311
|
+
BeliefSlot: {
|
|
312
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
313
|
+
subject: lowerPropositionSubject(pattern.subject),
|
|
314
|
+
predicate: lowerPredAtom(pattern.predicate)
|
|
202
315
|
}
|
|
203
316
|
};
|
|
204
317
|
case 'FilterClause':
|
|
205
318
|
return { Filter: { expression: lowerFilter(pattern.expression) } };
|
|
206
319
|
case 'NotClause':
|
|
207
|
-
return { Not:
|
|
320
|
+
return { Not: pattern.patterns.map(lowerWherePattern) };
|
|
208
321
|
case 'OptionalClause':
|
|
209
|
-
return {
|
|
210
|
-
Optional: nonEmptyBlock(pattern.patterns, 'OPTIONAL', pattern.range)
|
|
211
|
-
};
|
|
322
|
+
return { Optional: pattern.patterns.map(lowerWherePattern) };
|
|
212
323
|
case 'UnionClause':
|
|
213
|
-
return { Union:
|
|
324
|
+
return { Union: pattern.patterns.map(lowerWherePattern) };
|
|
214
325
|
}
|
|
215
326
|
}
|
|
216
|
-
function
|
|
217
|
-
if (
|
|
218
|
-
|
|
327
|
+
function lowerPropositionMatcher(tuple) {
|
|
328
|
+
if (tuple.id)
|
|
329
|
+
return { Id: lowerScalar(tuple.id) };
|
|
330
|
+
if (!tuple.subject || !tuple.predicate || !tuple.object) {
|
|
331
|
+
throw invalidSyntax('a Proposition expression is either (subject, predicate, object) or (id: ...)', tuple.range);
|
|
219
332
|
}
|
|
220
|
-
return
|
|
333
|
+
return {
|
|
334
|
+
Tuple: {
|
|
335
|
+
subject: lowerPropositionSubject(tuple.subject),
|
|
336
|
+
predicate: lowerPredicate(tuple.predicate),
|
|
337
|
+
object: lowerTerm(tuple.object)
|
|
338
|
+
}
|
|
339
|
+
};
|
|
221
340
|
}
|
|
222
341
|
/**
|
|
223
|
-
*
|
|
224
|
-
* expresses.
|
|
342
|
+
* Resolves the tuple a resolve-or-create statement needs.
|
|
225
343
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* written. A `null` value reads as "key absent", which is how the reference
|
|
230
|
-
* grammar treats it.
|
|
344
|
+
* `(id: ...)` is match-only: it names a Proposition that must already exist,
|
|
345
|
+
* so it cannot drive ENSURE PROPOSITION — or the ASSERT sugar that desugars
|
|
346
|
+
* through it — whose job is to create the tuple when it is absent.
|
|
231
347
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const seen = new Set();
|
|
237
|
-
for (const entry of matcher.entries) {
|
|
238
|
-
if (entry.key !== 'id' && entry.key !== 'type' && entry.key !== 'name') {
|
|
239
|
-
throw invalidSyntax(`invalid key in concept clause: ${entry.key} (expected id, type or name)`, entry.range);
|
|
240
|
-
}
|
|
241
|
-
if (seen.has(entry.key)) {
|
|
242
|
-
throw invalidSyntax(`duplicate key in concept clause: ${entry.key}`, entry.range);
|
|
243
|
-
}
|
|
244
|
-
seen.add(entry.key);
|
|
245
|
-
const value = matcherString(entry);
|
|
246
|
-
if (value === undefined)
|
|
247
|
-
continue;
|
|
248
|
-
if (entry.key === 'id')
|
|
249
|
-
id = value;
|
|
250
|
-
else if (entry.key === 'type')
|
|
251
|
-
type = value;
|
|
252
|
-
else
|
|
253
|
-
name = value;
|
|
254
|
-
}
|
|
255
|
-
if (id !== undefined) {
|
|
256
|
-
if (type !== undefined || name !== undefined) {
|
|
257
|
-
throw invalidSyntax('a concept clause cannot combine id with type or name', matcher.range);
|
|
258
|
-
}
|
|
259
|
-
return { ID: id };
|
|
260
|
-
}
|
|
261
|
-
if (type !== undefined && name !== undefined)
|
|
262
|
-
return { Object: { type, name } };
|
|
263
|
-
if (type !== undefined)
|
|
264
|
-
return { Type: type };
|
|
265
|
-
if (name !== undefined)
|
|
266
|
-
return { Name: name };
|
|
267
|
-
throw invalidSyntax('a concept clause must carry at least one of id, type or name', matcher.range);
|
|
268
|
-
}
|
|
269
|
-
function matcherString(entry) {
|
|
270
|
-
const value = entry.value;
|
|
271
|
-
if (value.kind === 'StringLiteral')
|
|
272
|
-
return value.parsed;
|
|
273
|
-
if (value.kind === 'NullLiteral')
|
|
274
|
-
return undefined;
|
|
275
|
-
throw invalidSyntax(`concept clause key ${entry.key} expects a quoted string or null`, value.range);
|
|
276
|
-
}
|
|
277
|
-
/** A KML endpoint must address exactly one element. */
|
|
278
|
-
function requireUniqueConcept(matcher, range) {
|
|
279
|
-
if ('ID' in matcher || 'Object' in matcher)
|
|
280
|
-
return matcher;
|
|
281
|
-
throw invalidSyntax(CONCEPT_IDENTITY_HELP, range);
|
|
282
|
-
}
|
|
283
|
-
function lowerPropositionMatcher(pattern) {
|
|
284
|
-
if (pattern.id) {
|
|
285
|
-
if (pattern.id.kind !== 'StringLiteral') {
|
|
286
|
-
throw invalidSyntax('a proposition id must be a quoted string', pattern.id.range);
|
|
287
|
-
}
|
|
288
|
-
return { ID: pattern.id.parsed };
|
|
348
|
+
function requireStructuralTuple(tuple, statement) {
|
|
349
|
+
if (tuple.id) {
|
|
350
|
+
throw invalidSyntax(`${statement} needs a (subject, predicate, object) tuple: (id: ...) only matches an ` +
|
|
351
|
+
'existing Proposition, and no structure can be created from an id', tuple.range);
|
|
289
352
|
}
|
|
290
|
-
|
|
291
|
-
|
|
353
|
+
const predicateExpression = tuple.predicate;
|
|
354
|
+
const predicate = lowerPredicate(predicateExpression);
|
|
355
|
+
if (!('Atom' in predicate)) {
|
|
356
|
+
throw invalidSyntax(`${statement} needs one exact predicate; alternation and hop quantifiers are KQL traversal forms`, tuple.predicate.range);
|
|
357
|
+
}
|
|
358
|
+
if ('Variable' in predicate.Atom) {
|
|
359
|
+
throw invalidSyntax(`${statement} needs an exact quoted predicate or :parameter; ?variables are KQL read-pattern syntax`, predicateExpression.range);
|
|
292
360
|
}
|
|
293
361
|
return {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
object: lowerEndpoint(pattern.object)
|
|
298
|
-
}
|
|
362
|
+
subject: lowerPropositionSubject(tuple.subject),
|
|
363
|
+
predicate: predicate.Atom,
|
|
364
|
+
object: lowerTerm(tuple.object)
|
|
299
365
|
};
|
|
300
366
|
}
|
|
301
|
-
function
|
|
302
|
-
|
|
367
|
+
function lowerPredicate(expr) {
|
|
368
|
+
const [only] = expr.atoms;
|
|
369
|
+
if (expr.atoms.length === 1 && only && !only.quantifier) {
|
|
370
|
+
return { Atom: lowerPredAtom(only.atom) };
|
|
371
|
+
}
|
|
372
|
+
const path = expr.atoms.map((atom) => ({
|
|
373
|
+
predicate: lowerPredAtom(atom.atom),
|
|
374
|
+
hops: atom.quantifier
|
|
375
|
+
? { min: atom.quantifier.min, max: atom.quantifier.max ?? null }
|
|
376
|
+
: null
|
|
377
|
+
}));
|
|
378
|
+
return { Path: path };
|
|
379
|
+
}
|
|
380
|
+
function lowerPredAtom(atom) {
|
|
381
|
+
switch (atom.kind) {
|
|
382
|
+
case 'StringLiteral':
|
|
383
|
+
return { Literal: atom.parsed };
|
|
384
|
+
case 'ParameterRef':
|
|
385
|
+
return { Param: paramName(atom.name) };
|
|
386
|
+
case 'VariableRef':
|
|
387
|
+
return { Variable: varName(atom.name, atom.range) };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function lowerTerm(term) {
|
|
391
|
+
switch (term.kind) {
|
|
303
392
|
case 'VariableRef':
|
|
304
|
-
return { Variable: varName(
|
|
305
|
-
case '
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
case '
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
return { MultiHop: { predicate: predicate.value, min: hop.min, max } };
|
|
393
|
+
return { Variable: varName(term.name, term.range) };
|
|
394
|
+
case 'ParameterRef':
|
|
395
|
+
return { Param: paramName(term.name) };
|
|
396
|
+
case 'ObjectPattern':
|
|
397
|
+
return { Match: lowerObjectMatcher(term) };
|
|
398
|
+
case 'PropositionTuple':
|
|
399
|
+
return { Proposition: lowerPropositionMatcher(term) };
|
|
400
|
+
default:
|
|
401
|
+
return { Literal: lowerKipValue(term) };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** A Proposition subject is always an Element reference, never a Literal. */
|
|
405
|
+
function lowerPropositionSubject(term) {
|
|
406
|
+
switch (term.kind) {
|
|
407
|
+
case 'StringLiteral':
|
|
408
|
+
case 'NumberLiteral':
|
|
409
|
+
case 'BooleanLiteral':
|
|
410
|
+
case 'NullLiteral':
|
|
411
|
+
throw invalidSyntax('a Proposition subject must be a local Element reference, never a Literal', term.range);
|
|
412
|
+
default:
|
|
413
|
+
return lowerTerm(term);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function lowerObjectMatcher(pattern) {
|
|
417
|
+
const matcher = {};
|
|
418
|
+
for (const member of pattern.members) {
|
|
419
|
+
if (Object.prototype.hasOwnProperty.call(matcher, member.key)) {
|
|
420
|
+
throw invalidSyntax(`duplicate match field ${member.key}`, member.range);
|
|
334
421
|
}
|
|
422
|
+
matcher[member.key] = lowerMatchValue(member.value);
|
|
423
|
+
}
|
|
424
|
+
return matcher;
|
|
425
|
+
}
|
|
426
|
+
function lowerMatchValue(expr) {
|
|
427
|
+
switch (expr.kind) {
|
|
428
|
+
case 'VariableRef':
|
|
429
|
+
return { Variable: varName(expr.name, expr.range) };
|
|
430
|
+
case 'ParameterRef':
|
|
431
|
+
return { Param: paramName(expr.name) };
|
|
432
|
+
case 'ArrayLiteral':
|
|
433
|
+
return { Array: expr.elements.map(lowerMatchValue) };
|
|
434
|
+
case 'ObjectPattern':
|
|
435
|
+
return { Match: lowerObjectMatcher(expr) };
|
|
436
|
+
case 'PropositionTuple':
|
|
437
|
+
return { Proposition: lowerPropositionMatcher(expr) };
|
|
438
|
+
default:
|
|
439
|
+
return { Literal: lowerKipValue(expr) };
|
|
335
440
|
}
|
|
336
441
|
}
|
|
337
442
|
// ---------------------------------------------------------------------------
|
|
338
|
-
//
|
|
443
|
+
// Filters
|
|
339
444
|
// ---------------------------------------------------------------------------
|
|
340
445
|
function lowerFilter(expr) {
|
|
341
446
|
switch (expr.kind) {
|
|
@@ -351,7 +456,7 @@ function lowerFilter(expr) {
|
|
|
351
456
|
}
|
|
352
457
|
const operator = COMPARISONS.get(expr.operator);
|
|
353
458
|
if (!operator) {
|
|
354
|
-
throw invalidSyntax(`
|
|
459
|
+
throw invalidSyntax(`unknown comparison operator ${expr.operator}`, expr.range);
|
|
355
460
|
}
|
|
356
461
|
return {
|
|
357
462
|
Comparison: {
|
|
@@ -362,498 +467,1146 @@ function lowerFilter(expr) {
|
|
|
362
467
|
};
|
|
363
468
|
}
|
|
364
469
|
case 'UnaryExpression':
|
|
365
|
-
|
|
470
|
+
if (expr.operator === '!') {
|
|
471
|
+
return { Not: lowerFilter(expr.operand) };
|
|
472
|
+
}
|
|
473
|
+
throw invalidSyntax('a filter must be a comparison, a logical combination, a negation or a function call', expr.range);
|
|
366
474
|
case 'FunctionCallExpr': {
|
|
367
|
-
const func = FILTER_FUNCTIONS.get(expr.name);
|
|
475
|
+
const func = FILTER_FUNCTIONS.get(expr.name.toUpperCase());
|
|
368
476
|
if (!func) {
|
|
369
|
-
throw invalidSyntax(
|
|
370
|
-
`STARTS_WITH, ENDS_WITH, REGEX, IN, IS_NULL or IS_NOT_NULL`, expr.range);
|
|
477
|
+
throw invalidSyntax(`${expr.name} is not a KIP filter function`, expr.range);
|
|
371
478
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
479
|
+
return {
|
|
480
|
+
Function: { func, args: expr.args.map(lowerFilterOperand) }
|
|
481
|
+
};
|
|
375
482
|
}
|
|
483
|
+
case 'AggregateExpr':
|
|
484
|
+
// An aggregate summarizes a solution set; a filter runs per candidate
|
|
485
|
+
// row, so there is no set for it to summarize yet.
|
|
486
|
+
throw invalidSyntax(`${expr.name} is an aggregate and cannot appear inside FILTER`, expr.range);
|
|
376
487
|
default:
|
|
377
|
-
throw invalidSyntax(
|
|
488
|
+
throw invalidSyntax(`a filter must be a comparison, a logical combination, a negation or a function call, found ${describeExpression(expr)}`, expr.range);
|
|
378
489
|
}
|
|
379
490
|
}
|
|
380
|
-
function
|
|
381
|
-
switch (
|
|
382
|
-
case '
|
|
383
|
-
case '
|
|
384
|
-
|
|
385
|
-
case '
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
case 'In':
|
|
391
|
-
if (args.length !== 2) {
|
|
392
|
-
throw invalidSyntax('IN requires exactly 2 arguments: IN(?expr, [values])', range);
|
|
393
|
-
}
|
|
394
|
-
if (!(args[1] && 'List' in args[1])) {
|
|
395
|
-
throw invalidSyntax('IN requires a literal list as its second argument', range);
|
|
491
|
+
function lowerFilterOperand(expr) {
|
|
492
|
+
switch (expr.kind) {
|
|
493
|
+
case 'VariableRef':
|
|
494
|
+
case 'FieldAccess':
|
|
495
|
+
return { Variable: lowerDotPath(expr) };
|
|
496
|
+
case 'ParameterRef':
|
|
497
|
+
return { Param: paramName(expr.name) };
|
|
498
|
+
case 'ArrayLiteral':
|
|
499
|
+
if (expr.trailingComma) {
|
|
500
|
+
throw invalidSyntax('a filter list does not allow a trailing comma', expr.range);
|
|
396
501
|
}
|
|
397
|
-
return;
|
|
398
|
-
case '
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
throw invalidSyntax('IS_NULL and IS_NOT_NULL require exactly 1 argument', range);
|
|
502
|
+
return { List: expr.elements.map(lowerFilterOperand) };
|
|
503
|
+
case 'UnaryExpression':
|
|
504
|
+
if (expr.operator === '-') {
|
|
505
|
+
return { Negate: lowerFilterOperand(expr.operand) };
|
|
402
506
|
}
|
|
507
|
+
throw invalidSyntax(`expected a filter operand, found ${describeExpression(expr)}`, expr.range);
|
|
508
|
+
case 'AggregateExpr':
|
|
509
|
+
// An aggregate summarizes a solution set; a filter runs per candidate
|
|
510
|
+
// row, so there is no set for it to summarize yet.
|
|
511
|
+
throw invalidSyntax(`${expr.name} is an aggregate and cannot appear inside FILTER`, expr.range);
|
|
512
|
+
case 'FunctionCallExpr':
|
|
513
|
+
case 'BinaryExpression':
|
|
514
|
+
throw invalidSyntax(`expected a filter operand, found ${describeExpression(expr)}`, expr.range);
|
|
515
|
+
default:
|
|
516
|
+
return { Literal: lowerKipValue(expr) };
|
|
403
517
|
}
|
|
404
518
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// KML
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
function lowerMutate(stmt) {
|
|
523
|
+
if (stmt.clauses.length === 0) {
|
|
524
|
+
throw invalidSyntax('MUTATE requires at least one mutation', stmt.range);
|
|
525
|
+
}
|
|
526
|
+
const clauses = stmt.clauses.flatMap((clause, i) => lowerMutationClause(clause, i));
|
|
527
|
+
assertUniqueHandles(clauses, stmt.range);
|
|
528
|
+
assertResolvedHandles(clauses, stmt.range);
|
|
529
|
+
return { explicit_transaction: true, clauses };
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Handles are block-local names. Two clauses claiming the same handle make
|
|
533
|
+
* every forward reference to it ambiguous, so the whole plan is rejected
|
|
534
|
+
* rather than resolved by position.
|
|
535
|
+
*/
|
|
536
|
+
function assertUniqueHandles(clauses, range) {
|
|
537
|
+
const seen = new Set();
|
|
538
|
+
for (const clause of clauses) {
|
|
539
|
+
const handle = handleOf(clause);
|
|
540
|
+
if (handle === null)
|
|
541
|
+
continue;
|
|
542
|
+
if (seen.has(handle)) {
|
|
543
|
+
throw invalidSyntax(`duplicate local handle ?${handle} in one mutation plan`, range);
|
|
544
|
+
}
|
|
545
|
+
seen.add(handle);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function handleOf(clause) {
|
|
549
|
+
if ('CreateConcept' in clause)
|
|
550
|
+
return clause.CreateConcept.handle;
|
|
551
|
+
if ('UpsertConcept' in clause)
|
|
552
|
+
return clause.UpsertConcept.handle;
|
|
553
|
+
if ('CreateEvidence' in clause)
|
|
554
|
+
return clause.CreateEvidence.handle;
|
|
555
|
+
if ('CreateAssertion' in clause)
|
|
556
|
+
return clause.CreateAssertion.handle;
|
|
557
|
+
if ('CreateActivity' in clause)
|
|
558
|
+
return clause.CreateActivity.handle;
|
|
559
|
+
if ('EnsureProposition' in clause)
|
|
560
|
+
return clause.EnsureProposition.handle;
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Every executable `Handle` must be created by this mutation plan or bound by
|
|
565
|
+
* that clause's WHERE. Parameters remain runtime bindings and are unaffected.
|
|
566
|
+
*/
|
|
567
|
+
function assertResolvedHandles(clauses, range) {
|
|
568
|
+
const planHandles = new Set();
|
|
569
|
+
for (const clause of clauses) {
|
|
570
|
+
const handle = handleOf(clause);
|
|
571
|
+
if (handle !== null)
|
|
572
|
+
planHandles.add(handle);
|
|
408
573
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
574
|
+
for (const clause of clauses) {
|
|
575
|
+
const allowed = new Set(planHandles);
|
|
576
|
+
const body = Object.values(clause)[0];
|
|
577
|
+
collectWhereVariables(body.where_clauses, allowed);
|
|
578
|
+
const referenced = new Set();
|
|
579
|
+
collectTaggedHandles(body, referenced);
|
|
580
|
+
for (const handle of referenced) {
|
|
581
|
+
if (!allowed.has(handle)) {
|
|
582
|
+
throw invalidSyntax(`?${handle} is not bound by this command's mutation outputs or WHERE clause`, range);
|
|
583
|
+
}
|
|
412
584
|
}
|
|
413
|
-
return { List: expr.elements.map(lowerKipValue) };
|
|
414
585
|
}
|
|
415
|
-
return { Literal: lowerKipValue(expr) };
|
|
416
586
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
587
|
+
function collectWhereVariables(value, out) {
|
|
588
|
+
if (Array.isArray(value)) {
|
|
589
|
+
for (const item of value)
|
|
590
|
+
collectWhereVariables(item, out);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!value || typeof value !== 'object')
|
|
594
|
+
return;
|
|
595
|
+
const record = value;
|
|
596
|
+
if (typeof record.variable === 'string')
|
|
597
|
+
out.add(record.variable);
|
|
598
|
+
// Pattern terms and predicate atoms use the tagged `{ Variable: name }`
|
|
599
|
+
// shape. Filter operands also use `Variable`, but carry a DotPathVar object
|
|
600
|
+
// rather than a string, so they cannot accidentally introduce a binding.
|
|
601
|
+
if (typeof record.Variable === 'string')
|
|
602
|
+
out.add(record.Variable);
|
|
603
|
+
for (const child of Object.values(record))
|
|
604
|
+
collectWhereVariables(child, out);
|
|
605
|
+
}
|
|
606
|
+
function collectTaggedHandles(value, out) {
|
|
607
|
+
if (Array.isArray(value)) {
|
|
608
|
+
for (const item of value)
|
|
609
|
+
collectTaggedHandles(item, out);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!value || typeof value !== 'object')
|
|
613
|
+
return;
|
|
614
|
+
const record = value;
|
|
615
|
+
if (typeof record.Handle === 'string')
|
|
616
|
+
out.add(record.Handle);
|
|
617
|
+
for (const child of Object.values(record))
|
|
618
|
+
collectTaggedHandles(child, out);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* One source statement may lower to several clauses; `ASSERT` is the case.
|
|
622
|
+
*
|
|
623
|
+
* `seq` is the clause's position in its plan, used only to keep synthetic
|
|
624
|
+
* handles distinct between two handle-less ASSERTs in the same transaction.
|
|
625
|
+
*/
|
|
626
|
+
function lowerMutationClause(stmt, seq) {
|
|
627
|
+
switch (stmt.kind) {
|
|
628
|
+
case 'CreateConceptStatement':
|
|
629
|
+
return [{ CreateConcept: lowerCreateConcept(stmt) }];
|
|
630
|
+
case 'UpsertConceptStatement':
|
|
631
|
+
return [{ UpsertConcept: lowerUpsertConcept(stmt) }];
|
|
632
|
+
case 'EnsurePropositionStatement':
|
|
633
|
+
return [{ EnsureProposition: lowerEnsureProposition(stmt) }];
|
|
634
|
+
case 'AssertStatement':
|
|
635
|
+
return lowerAssertSugar(stmt, seq);
|
|
636
|
+
case 'CreateEvidenceStatement':
|
|
637
|
+
return [{ CreateEvidence: lowerRecordCreate(stmt) }];
|
|
638
|
+
case 'CreateAssertionStatement':
|
|
639
|
+
return [{ CreateAssertion: lowerRecordCreate(stmt) }];
|
|
640
|
+
case 'CreateActivityStatement':
|
|
641
|
+
return [{ CreateActivity: lowerRecordCreate(stmt) }];
|
|
642
|
+
case 'UpdateStatement':
|
|
643
|
+
return [{ Update: lowerUpdate(stmt) }];
|
|
644
|
+
case 'RetractAssertionStatement':
|
|
645
|
+
return [
|
|
646
|
+
{
|
|
647
|
+
RetractAssertion: {
|
|
648
|
+
target: lowerElementRef(stmt.target),
|
|
649
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
650
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
651
|
+
expect_state: stmt.expectState
|
|
652
|
+
? lowerScalar(stmt.expectState.value)
|
|
653
|
+
: null
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
];
|
|
657
|
+
case 'SupersedeAssertionStatement':
|
|
658
|
+
return [
|
|
659
|
+
{
|
|
660
|
+
SupersedeAssertion: {
|
|
661
|
+
target: lowerElementRef(stmt.target),
|
|
662
|
+
by: lowerElementRef(stmt.by),
|
|
663
|
+
expect_state: stmt.expectState
|
|
664
|
+
? lowerScalar(stmt.expectState.value)
|
|
665
|
+
: null
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
];
|
|
669
|
+
case 'CorrectEvidenceStatement':
|
|
670
|
+
return [
|
|
671
|
+
{
|
|
672
|
+
CorrectEvidence: {
|
|
673
|
+
target: lowerElementRef(stmt.target),
|
|
674
|
+
by: lowerElementRef(stmt.by),
|
|
675
|
+
expect_state: stmt.expectState
|
|
676
|
+
? lowerScalar(stmt.expectState.value)
|
|
677
|
+
: null
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
];
|
|
681
|
+
case 'TransitionActivityStatement':
|
|
682
|
+
return [{ TransitionActivity: lowerTransition(stmt) }];
|
|
683
|
+
case 'SetRetentionStatement':
|
|
684
|
+
return [
|
|
685
|
+
{
|
|
686
|
+
SetRetention: {
|
|
687
|
+
target: lowerElementRef(stmt.target),
|
|
688
|
+
values: lowerAssignments(stmt.assignments, null),
|
|
689
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
690
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
691
|
+
expect_version: stmt.expectVersion
|
|
692
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
693
|
+
: null
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
];
|
|
697
|
+
case 'ArchiveStatement':
|
|
698
|
+
return [{ Archive: lowerRemoval(stmt) }];
|
|
699
|
+
case 'TombstoneStatement':
|
|
700
|
+
return [{ Tombstone: lowerRemoval(stmt) }];
|
|
701
|
+
case 'PurgeStatement':
|
|
702
|
+
return [{ Purge: lowerPurge(stmt) }];
|
|
703
|
+
case 'MergeConceptStatement':
|
|
704
|
+
return [
|
|
705
|
+
{
|
|
706
|
+
MergeConcept: {
|
|
707
|
+
source: lowerElementRef(stmt.source),
|
|
708
|
+
into: lowerElementRef(stmt.into),
|
|
709
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
710
|
+
expect_version: stmt.expectVersion
|
|
711
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
712
|
+
: null
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function lowerCreateConcept(stmt) {
|
|
719
|
+
return {
|
|
720
|
+
handle: varName(stmt.handle.name, stmt.handle.range),
|
|
721
|
+
type: stmt.type ? lowerSymbol(stmt.type.value) : null,
|
|
722
|
+
client_key: stmt.clientKey ? lowerScalar(stmt.clientKey.value) : null,
|
|
723
|
+
name: stmt.name ? lowerScalar(stmt.name.value) : null,
|
|
724
|
+
set_fields: stmt.setFields
|
|
725
|
+
? lowerAssignments(stmt.setFields.assignments, null)
|
|
435
726
|
: null,
|
|
436
|
-
|
|
437
|
-
?
|
|
438
|
-
predicate: item.predicate,
|
|
439
|
-
object: requireIdentityTarget(lowerEndpoint(item.target), item.range),
|
|
440
|
-
metadata: lowerMetadata(item.metadata)
|
|
441
|
-
}))
|
|
727
|
+
set_attributes: stmt.setAttributes
|
|
728
|
+
? lowerAssignments(stmt.setAttributes.assignments, null)
|
|
442
729
|
: null,
|
|
443
|
-
|
|
730
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
731
|
+
set_structural: stmt.setStructural
|
|
732
|
+
? lowerStructural(stmt.setStructural, null)
|
|
733
|
+
: null
|
|
444
734
|
};
|
|
445
|
-
const version = lowerExpectVersion(block);
|
|
446
|
-
if (version !== undefined)
|
|
447
|
-
out.expect_version = version;
|
|
448
|
-
return out;
|
|
449
735
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
736
|
+
/**
|
|
737
|
+
* Whether a matcher pins exactly one Concept.
|
|
738
|
+
*
|
|
739
|
+
* Only `id` and `key` identify, and only when they carry a value that is one
|
|
740
|
+
* value: a literal, or a parameter the runtime binds to one. Anything else —
|
|
741
|
+
* a variable, a nested pattern, a list — describes candidates.
|
|
742
|
+
*/
|
|
743
|
+
function hasStableIdentity(match) {
|
|
744
|
+
for (const field of ['id', 'key']) {
|
|
745
|
+
const value = match[field];
|
|
746
|
+
if (value && ('Literal' in value || 'Param' in value))
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
function lowerUpsertConcept(stmt) {
|
|
752
|
+
const match = stmt.match ? lowerObjectMatcher(stmt.match.pattern) : null;
|
|
753
|
+
// Identity for an upsert is `id` or `key`, spelled as a literal or a
|
|
754
|
+
// parameter. Three things are refused here, and they are the same mistake at
|
|
755
|
+
// different depths:
|
|
756
|
+
//
|
|
757
|
+
// - no MATCH at all, which would make UPSERT mean "create, always";
|
|
758
|
+
// - a name-only match, because names are mutable grounding state with
|
|
759
|
+
// duplicates allowed, so "the Concept named X" can silently address a
|
|
760
|
+
// different node over time;
|
|
761
|
+
// - an identity whose value is a variable, which is a *set* of candidates
|
|
762
|
+
// rather than one element — an upsert resolving it would pick a winner.
|
|
763
|
+
//
|
|
764
|
+
// A match may carry other fields beside the identity; they narrow, they do
|
|
765
|
+
// not identify.
|
|
766
|
+
if (!match || !hasStableIdentity(match)) {
|
|
767
|
+
throw invalidSyntax('UPSERT CONCEPT requires a MATCH on a stable identity: {id: <literal-or-parameter>} ' +
|
|
768
|
+
'or {key: <literal-or-parameter>} — name is mutable grounding state and never ' +
|
|
769
|
+
'identifies a Concept, and a variable names a set rather than an element', stmt.match ? stmt.match.range : stmt.range);
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
handle: varName(stmt.handle.name, stmt.handle.range),
|
|
773
|
+
match,
|
|
774
|
+
expect_version: stmt.expectVersion
|
|
775
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
776
|
+
: null,
|
|
777
|
+
set_fields: stmt.setFields
|
|
778
|
+
? lowerAssignments(stmt.setFields.assignments, null)
|
|
779
|
+
: null,
|
|
780
|
+
set_attributes: stmt.setAttributes
|
|
781
|
+
? lowerAssignments(stmt.setAttributes.assignments, null)
|
|
458
782
|
: null,
|
|
459
|
-
|
|
783
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
784
|
+
unset_attributes: stmt.unsetAttributes
|
|
785
|
+
? lowerUnsetFields(stmt.unsetAttributes.fields)
|
|
786
|
+
: null,
|
|
787
|
+
unset_facets: stmt.unsetFacets.map((f) => ({
|
|
788
|
+
facet: lowerSymbol(f.facet),
|
|
789
|
+
fields: lowerUnsetFields(f.fields)
|
|
790
|
+
})),
|
|
791
|
+
set_structural: stmt.setStructural
|
|
792
|
+
? lowerStructural(stmt.setStructural, null)
|
|
793
|
+
: null,
|
|
794
|
+
unset_structural: stmt.unsetStructural
|
|
795
|
+
? lowerStructuralRemovals(stmt.unsetStructural, null)
|
|
796
|
+
: null
|
|
460
797
|
};
|
|
461
|
-
const version = lowerExpectVersion(block);
|
|
462
|
-
if (version !== undefined)
|
|
463
|
-
out.expect_version = version;
|
|
464
|
-
return out;
|
|
465
798
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
799
|
+
function lowerEnsureProposition(stmt) {
|
|
800
|
+
const triple = requireStructuralTuple(stmt.tuple, 'ENSURE PROPOSITION');
|
|
801
|
+
return {
|
|
802
|
+
handle: stmt.handle ? varName(stmt.handle.name, stmt.handle.range) : null,
|
|
803
|
+
...triple,
|
|
804
|
+
expect_version: stmt.expectVersion
|
|
805
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
806
|
+
: null
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function lowerRecordCreate(stmt) {
|
|
810
|
+
const fields = stmt.setFields
|
|
811
|
+
? lowerAssignments(stmt.setFields.assignments, null)
|
|
812
|
+
: null;
|
|
813
|
+
return {
|
|
814
|
+
handle: varName(stmt.handle.name, stmt.handle.range),
|
|
815
|
+
client_key: stmt.clientKey ? lowerScalar(stmt.clientKey.value) : null,
|
|
816
|
+
set_fields: fields,
|
|
817
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
818
|
+
set_structural: stmt.setStructural
|
|
819
|
+
? lowerStructural(stmt.setStructural, null)
|
|
820
|
+
: null
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Desugars `ASSERT` into exactly what the Spec defines it as (§55.1):
|
|
825
|
+
* `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `SUPERSEDE` when written.
|
|
826
|
+
*
|
|
827
|
+
* Nothing else is fabricated. The sugar exists because recording an
|
|
828
|
+
* attributed claim is the hot path, not because it means anything new.
|
|
829
|
+
*/
|
|
830
|
+
function lowerAssertSugar(stmt, seq) {
|
|
831
|
+
const members = new Map();
|
|
832
|
+
for (const entry of stmt.assignments.entries) {
|
|
833
|
+
if (members.has(entry.key)) {
|
|
834
|
+
throw invalidSyntax(`duplicate ASSERT member ${entry.key}`, entry.range);
|
|
835
|
+
}
|
|
836
|
+
members.set(entry.key, entry.value);
|
|
837
|
+
}
|
|
838
|
+
const known = new Set([
|
|
839
|
+
'by',
|
|
840
|
+
'mode',
|
|
841
|
+
'stance',
|
|
842
|
+
'confidence',
|
|
843
|
+
'at',
|
|
844
|
+
'valid',
|
|
845
|
+
'evidence',
|
|
846
|
+
'key'
|
|
847
|
+
]);
|
|
848
|
+
for (const [key, value] of members) {
|
|
849
|
+
if (!known.has(key)) {
|
|
850
|
+
throw invalidSyntax(`${key} is not an ASSERT member; expected one of ${[...known].join(', ')}`, value.range);
|
|
851
|
+
}
|
|
472
852
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
853
|
+
// `by` names whose stance this is, and `mode` says how it was arrived at.
|
|
854
|
+
// Neither has a safe default: guessing the actor would forge attribution,
|
|
855
|
+
// and guessing the mode would turn hearsay into observation.
|
|
856
|
+
const by = members.get('by');
|
|
857
|
+
if (!by) {
|
|
858
|
+
throw invalidSyntax('ASSERT requires by: <semantic actor> — an Assertion without an assertor has no epistemic owner', stmt.assignments.range);
|
|
476
859
|
}
|
|
477
|
-
|
|
860
|
+
const mode = members.get('mode');
|
|
861
|
+
if (!mode) {
|
|
862
|
+
throw invalidSyntax('ASSERT requires mode: one of observed, stated, inferred, predicted, hypothetical, imported', stmt.assignments.range);
|
|
863
|
+
}
|
|
864
|
+
// The Proposition handle is synthesized, so it must collide with neither a
|
|
865
|
+
// user handle nor another ASSERT in the same plan. `#` cannot occur in a KIP
|
|
866
|
+
// identifier, which rules out the first; `seq` is the clause position, which
|
|
867
|
+
// rules out the second — two handle-less ASSERTs in one MUTATE are ordinary
|
|
868
|
+
// input, not a name clash.
|
|
869
|
+
const assertionHandle = stmt.handle
|
|
870
|
+
? varName(stmt.handle.name, stmt.handle.range)
|
|
871
|
+
: `#assert${seq}`;
|
|
872
|
+
const propositionHandle = `${assertionHandle}#proposition`;
|
|
873
|
+
const triple = requireStructuralTuple(stmt.tuple, 'ASSERT');
|
|
874
|
+
const clauses = [
|
|
875
|
+
{
|
|
876
|
+
EnsureProposition: {
|
|
877
|
+
handle: propositionHandle,
|
|
878
|
+
...triple,
|
|
879
|
+
expect_version: null
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
];
|
|
883
|
+
const fields = [
|
|
884
|
+
['proposition', { Handle: propositionHandle }],
|
|
885
|
+
['asserted_by', lowerMutationValue(by, null)],
|
|
886
|
+
['mode', lowerMutationValue(mode, null)],
|
|
887
|
+
// The normative expansion carries a stance even when the source omitted
|
|
888
|
+
// one, so the default is materialized here rather than left for the
|
|
889
|
+
// engine to re-derive.
|
|
890
|
+
[
|
|
891
|
+
'stance',
|
|
892
|
+
members.has('stance')
|
|
893
|
+
? lowerMutationValue(members.get('stance'), null)
|
|
894
|
+
: { Value: { String: 'support' } }
|
|
895
|
+
]
|
|
896
|
+
];
|
|
897
|
+
const optional = [
|
|
898
|
+
['confidence', 'confidence'],
|
|
899
|
+
['at', 'asserted_at'],
|
|
900
|
+
['valid', 'valid_time']
|
|
901
|
+
];
|
|
902
|
+
for (const [member, field] of optional) {
|
|
903
|
+
const value = members.get(member);
|
|
904
|
+
if (value)
|
|
905
|
+
fields.push([field, lowerMutationValue(value, null)]);
|
|
906
|
+
}
|
|
907
|
+
// `evidence` is a reserved Core *structural* field, not a plain one: the
|
|
908
|
+
// normative desugaring emits `("evidence", ref) {role: "support"}`. An array
|
|
909
|
+
// cites several artifacts, so it becomes one role-qualified edge each.
|
|
910
|
+
const evidenceExpr = members.get('evidence');
|
|
911
|
+
const evidenceEdges = evidenceExpr === undefined
|
|
912
|
+
? []
|
|
913
|
+
: (evidenceExpr.kind === 'ArrayLiteral'
|
|
914
|
+
? evidenceExpr.elements
|
|
915
|
+
: [evidenceExpr]).map((ref) => ({
|
|
916
|
+
field: { Name: 'evidence' },
|
|
917
|
+
value: lowerMutationValue(ref, null),
|
|
918
|
+
options: { role: { Value: { String: 'support' } } }
|
|
919
|
+
}));
|
|
920
|
+
const clientKeyExpr = members.get('key');
|
|
921
|
+
clauses.push({
|
|
922
|
+
CreateAssertion: {
|
|
923
|
+
handle: assertionHandle,
|
|
924
|
+
client_key: clientKeyExpr ? lowerScalarExpression(clientKeyExpr) : null,
|
|
925
|
+
set_fields: fields,
|
|
926
|
+
set_facets: [],
|
|
927
|
+
set_structural: evidenceEdges.length > 0 ? evidenceEdges : null
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
if (stmt.superseding) {
|
|
931
|
+
clauses.push({
|
|
932
|
+
SupersedeAssertion: {
|
|
933
|
+
target: lowerElementRef(stmt.superseding),
|
|
934
|
+
by: { Handle: assertionHandle },
|
|
935
|
+
expect_state: null
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return clauses;
|
|
478
940
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
941
|
+
function lowerTransition(stmt) {
|
|
942
|
+
let setFields = null;
|
|
943
|
+
let setStructural = null;
|
|
944
|
+
for (const clause of stmt.finalize) {
|
|
945
|
+
if (clause.kind === 'SetFieldsClause') {
|
|
946
|
+
if (setFields) {
|
|
947
|
+
throw invalidSyntax('duplicate SET FIELDS clause', clause.range);
|
|
948
|
+
}
|
|
949
|
+
setFields = lowerAssignments(clause.assignments, null);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
if (setStructural) {
|
|
953
|
+
throw invalidSyntax('duplicate SET STRUCTURAL clause', clause.range);
|
|
954
|
+
}
|
|
955
|
+
setStructural = lowerStructural(clause, null);
|
|
956
|
+
}
|
|
486
957
|
}
|
|
487
|
-
|
|
488
|
-
|
|
958
|
+
return {
|
|
959
|
+
target: lowerElementRef(stmt.target),
|
|
960
|
+
to: lowerScalar(stmt.to),
|
|
961
|
+
set_fields: setFields,
|
|
962
|
+
set_structural: setStructural,
|
|
963
|
+
expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
|
|
964
|
+
};
|
|
489
965
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
966
|
+
function lowerRemoval(stmt) {
|
|
967
|
+
return {
|
|
968
|
+
target: lowerElementRef(stmt.target),
|
|
969
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
970
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
971
|
+
expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function lowerPurge(stmt) {
|
|
975
|
+
if (stmt.confirm.parsed !== 'PURGE') {
|
|
976
|
+
throw invalidSyntax('PURGE must be confirmed with the exact literal "PURGE"', stmt.confirm.range);
|
|
495
977
|
}
|
|
496
|
-
|
|
497
|
-
|
|
978
|
+
return {
|
|
979
|
+
target: lowerElementRef(stmt.target),
|
|
980
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
981
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
982
|
+
reference_policy: stmt.referencePolicy
|
|
983
|
+
? lowerScalar(stmt.referencePolicy)
|
|
984
|
+
: null,
|
|
985
|
+
confirm: 'PURGE'
|
|
986
|
+
};
|
|
498
987
|
}
|
|
988
|
+
// ---------------------------------------------------------------------------
|
|
989
|
+
// UPDATE
|
|
990
|
+
// ---------------------------------------------------------------------------
|
|
499
991
|
function lowerUpdate(stmt) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const set_metadata = lowerUpdateEntries(stmt.setMetadata, target);
|
|
503
|
-
if (!set_attributes && !set_metadata) {
|
|
504
|
-
throw invalidSyntax('UPDATE needs at least one SET ATTRIBUTES or SET METADATA block', stmt.range);
|
|
992
|
+
if (stmt.actions.length === 0) {
|
|
993
|
+
throw invalidSyntax('UPDATE requires at least one SET or UNSET action', stmt.range);
|
|
505
994
|
}
|
|
995
|
+
const target = lowerElementRef(stmt.target);
|
|
996
|
+
const targetVar = 'Handle' in target ? target.Handle : null;
|
|
997
|
+
const kind = targetVar && stmt.where ? boundKindOf(targetVar, stmt.where.patterns) : null;
|
|
998
|
+
const actions = stmt.actions.map((action) => lowerUpdateAction(action, targetVar, kind));
|
|
506
999
|
return {
|
|
507
1000
|
target,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1001
|
+
expect_version: stmt.expectVersion
|
|
1002
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
1003
|
+
: null,
|
|
1004
|
+
actions,
|
|
1005
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
1006
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
512
1007
|
};
|
|
513
1008
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1009
|
+
function boundKindOf(variable, patterns) {
|
|
1010
|
+
for (const pattern of patterns) {
|
|
1011
|
+
switch (pattern.kind) {
|
|
1012
|
+
case 'AssertionPattern':
|
|
1013
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1014
|
+
return 'assertion';
|
|
1015
|
+
}
|
|
1016
|
+
break;
|
|
1017
|
+
case 'EvidencePattern':
|
|
1018
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1019
|
+
return 'evidence';
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
case 'ActivityPattern':
|
|
1023
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1024
|
+
return 'activity';
|
|
1025
|
+
}
|
|
1026
|
+
break;
|
|
1027
|
+
case 'ConceptPattern':
|
|
1028
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1029
|
+
return 'concept';
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
case 'PropositionPattern':
|
|
1033
|
+
if (pattern.variable &&
|
|
1034
|
+
varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1035
|
+
return 'proposition';
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1038
|
+
case 'NotClause':
|
|
1039
|
+
case 'OptionalClause':
|
|
1040
|
+
case 'UnionClause': {
|
|
1041
|
+
const nested = boundKindOf(variable, pattern.patterns);
|
|
1042
|
+
if (nested)
|
|
1043
|
+
return nested;
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
519
1047
|
}
|
|
520
|
-
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function lowerUpdateAction(action, targetVar, kind) {
|
|
1051
|
+
switch (action.kind) {
|
|
1052
|
+
case 'SetFieldsClause': {
|
|
1053
|
+
const assignments = lowerAssignments(action.assignments, targetVar);
|
|
1054
|
+
for (const entry of action.assignments.entries) {
|
|
1055
|
+
guardImmutableField(entry.key, kind, entry.range);
|
|
1056
|
+
}
|
|
1057
|
+
return { SetFields: assignments };
|
|
1058
|
+
}
|
|
1059
|
+
case 'SetAttributesClause': {
|
|
1060
|
+
const assignments = lowerAssignments(action.assignments, targetVar);
|
|
1061
|
+
for (const entry of action.assignments.entries) {
|
|
1062
|
+
guardProtectedField(entry.key, entry.range);
|
|
1063
|
+
}
|
|
1064
|
+
return { SetAttributes: assignments };
|
|
1065
|
+
}
|
|
1066
|
+
case 'SetFacetClause':
|
|
1067
|
+
return { SetFacet: lowerFacet(action, targetVar) };
|
|
1068
|
+
case 'UnsetAttributesClause':
|
|
1069
|
+
return { UnsetAttributes: lowerUnsetFields(action.fields) };
|
|
1070
|
+
case 'UnsetFacetClause':
|
|
1071
|
+
return {
|
|
1072
|
+
UnsetFacet: {
|
|
1073
|
+
facet: lowerSymbol(action.facet),
|
|
1074
|
+
fields: lowerUnsetFields(action.fields)
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
case 'SetStructuralClause':
|
|
1078
|
+
guardStructuralMutation('SET STRUCTURAL', kind, action.range);
|
|
1079
|
+
return { SetStructural: lowerStructural(action, targetVar) };
|
|
1080
|
+
case 'UnsetStructuralClause':
|
|
1081
|
+
guardStructuralMutation('UNSET STRUCTURAL', kind, action.range);
|
|
1082
|
+
return { UnsetStructural: lowerStructuralRemovals(action, targetVar) };
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Structural mutation reaches mutable Concept topology only (Spec §17.5).
|
|
1087
|
+
* Record kinds keep their topology: an Assertion's citations and an
|
|
1088
|
+
* Evidence's lineage are immutable payload, a Proposition has no structural
|
|
1089
|
+
* fields, and a pending Activity finalizes through TRANSITION ACTIVITY.
|
|
1090
|
+
*/
|
|
1091
|
+
function guardStructuralMutation(verb, kind, range) {
|
|
1092
|
+
switch (kind) {
|
|
1093
|
+
case 'assertion':
|
|
1094
|
+
throw invalidSyntax(`${verb} cannot change an Assertion's citations: they are immutable payload — record a new Assertion with SUPERSEDING`, range);
|
|
1095
|
+
case 'evidence':
|
|
1096
|
+
throw invalidSyntax(`${verb} cannot change Evidence topology: correct it with CORRECT EVIDENCE :old BY :new`, range);
|
|
1097
|
+
case 'proposition':
|
|
1098
|
+
throw invalidSyntax(`${verb} has no target on a Proposition: a Proposition is its tuple and carries no structural fields`, range);
|
|
1099
|
+
case 'activity':
|
|
1100
|
+
throw invalidSyntax(`${verb} cannot change Activity topology: finalize a pending Activity with TRANSITION ACTIVITY ... SET STRUCTURAL; a terminal Activity is immutable`, range);
|
|
1101
|
+
default:
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** Engine-owned state is never author-writable, whatever the element kind. */
|
|
1106
|
+
function guardProtectedField(field, range) {
|
|
1107
|
+
if (PROTECTED_FIELDS.has(field)) {
|
|
1108
|
+
throw invalidSyntax(`${field} is engine-maintained state and cannot be written by a mutation`, range);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function guardImmutableField(field, kind, range) {
|
|
1112
|
+
guardProtectedField(field, range);
|
|
1113
|
+
if (kind === 'assertion' && ASSERTION_IMMUTABLE.has(field)) {
|
|
1114
|
+
throw invalidSyntax(`${field} is immutable Assertion payload: record the change as a new Assertion with SUPERSEDING, ` +
|
|
1115
|
+
'never by rewriting the old one', range);
|
|
1116
|
+
}
|
|
1117
|
+
if (kind === 'evidence' && EVIDENCE_IMMUTABLE.has(field)) {
|
|
1118
|
+
throw invalidSyntax(`${field} is immutable Evidence payload: correct it with CORRECT EVIDENCE :old BY :new`, range);
|
|
1119
|
+
}
|
|
1120
|
+
if (kind === 'proposition' && PROPOSITION_IMMUTABLE.has(field)) {
|
|
1121
|
+
throw invalidSyntax(`${field} is part of the immutable Proposition tuple: a different tuple is a different Proposition`, range);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// ---------------------------------------------------------------------------
|
|
1125
|
+
// Assignments, facets, structural edges
|
|
1126
|
+
// ---------------------------------------------------------------------------
|
|
1127
|
+
function lowerAssignments(object, targetVar) {
|
|
521
1128
|
const seen = new Set();
|
|
522
|
-
|
|
1129
|
+
const out = [];
|
|
1130
|
+
for (const entry of object.entries) {
|
|
1131
|
+
guardProtectedField(entry.key, entry.range);
|
|
523
1132
|
if (seen.has(entry.key)) {
|
|
524
|
-
throw invalidSyntax(`duplicate
|
|
1133
|
+
throw invalidSyntax(`duplicate assignment for ${entry.key}`, entry.range);
|
|
525
1134
|
}
|
|
526
1135
|
seen.add(entry.key);
|
|
527
|
-
out.push([entry.key,
|
|
1136
|
+
out.push([entry.key, lowerMutationValue(entry.value, targetVar)]);
|
|
528
1137
|
}
|
|
529
1138
|
return out;
|
|
530
1139
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1140
|
+
function lowerFacet(clause, targetVar) {
|
|
1141
|
+
return {
|
|
1142
|
+
facet: lowerSymbol(clause.facet),
|
|
1143
|
+
values: lowerAssignments(clause.assignments, targetVar)
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function lowerStructural(clause, targetVar) {
|
|
1147
|
+
return clause.assignments.map((assignment) => ({
|
|
1148
|
+
field: lowerSymbol(assignment.field),
|
|
1149
|
+
value: lowerMutationValue(assignment.value, targetVar),
|
|
1150
|
+
options: assignment.options ? lowerBoundObject(assignment.options) : null
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
function lowerStructuralRemovals(clause, targetVar) {
|
|
1154
|
+
if (clause.removals.length === 0) {
|
|
1155
|
+
throw invalidSyntax('UNSET STRUCTURAL removes named references; list at least one (field, target)', clause.range);
|
|
536
1156
|
}
|
|
537
|
-
return
|
|
1157
|
+
return clause.removals.map((removal) => ({
|
|
1158
|
+
field: lowerSymbol(removal.field),
|
|
1159
|
+
value: lowerMutationValue(removal.value, targetVar)
|
|
1160
|
+
}));
|
|
538
1161
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
throw invalidSyntax(`
|
|
544
|
-
}
|
|
545
|
-
const expected = UPDATE_ARITY[func];
|
|
546
|
-
if (expr.args.length !== expected) {
|
|
547
|
-
throw invalidSyntax(`${expr.name} requires exactly ${expected} arguments, got ${expr.args.length}`, expr.range);
|
|
1162
|
+
function lowerUnsetFields(fields) {
|
|
1163
|
+
const seen = new Set();
|
|
1164
|
+
for (const field of fields) {
|
|
1165
|
+
if (seen.has(field.name)) {
|
|
1166
|
+
throw invalidSyntax(`duplicate field ${field.name}`, field.range);
|
|
548
1167
|
}
|
|
549
|
-
|
|
1168
|
+
guardProtectedField(field.name, field.range);
|
|
1169
|
+
seen.add(field.name);
|
|
550
1170
|
}
|
|
551
|
-
|
|
552
|
-
|
|
1171
|
+
return [...seen];
|
|
1172
|
+
}
|
|
1173
|
+
function lowerMutationValue(expr, targetVar) {
|
|
1174
|
+
if (expr.kind === 'FunctionCallExpr') {
|
|
1175
|
+
return { Expr: lowerUpdateExpr(expr, targetVar) };
|
|
553
1176
|
}
|
|
554
|
-
if (expr.kind === '
|
|
555
|
-
|
|
1177
|
+
if (expr.kind === 'AggregateExpr') {
|
|
1178
|
+
throw invalidSyntax(`${expr.name} is an aggregate and cannot appear in an assignment`, expr.range);
|
|
556
1179
|
}
|
|
557
|
-
|
|
1180
|
+
return lowerBoundValue(expr, targetVar);
|
|
558
1181
|
}
|
|
559
1182
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* join the engine happened to visit, so a bulk UPDATE would stop being
|
|
564
|
-
* deterministic and order-independent.
|
|
1183
|
+
* Lowers a `data_value`, keeping structure only where something still needs
|
|
1184
|
+
* binding. A wholly literal subtree collapses to one `Value`, so an engine
|
|
1185
|
+
* that has nothing to substitute never walks a binding tree.
|
|
565
1186
|
*/
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
checkUpdateExprTargets(arg, target, key);
|
|
1187
|
+
function lowerBoundValue(expr, targetVar) {
|
|
1188
|
+
switch (expr.kind) {
|
|
1189
|
+
case 'ParameterRef':
|
|
1190
|
+
return { Param: paramName(expr.name) };
|
|
1191
|
+
case 'VariableRef':
|
|
1192
|
+
return { Handle: varName(expr.name, expr.range) };
|
|
1193
|
+
case 'FieldAccess': {
|
|
1194
|
+
const path = lowerDotPath(expr);
|
|
1195
|
+
guardOwnField(path, targetVar, expr.range);
|
|
1196
|
+
return { Variable: path };
|
|
577
1197
|
}
|
|
1198
|
+
case 'ArrayLiteral':
|
|
1199
|
+
return isFullyLiteral(expr)
|
|
1200
|
+
? { Value: lowerKipValue(expr) }
|
|
1201
|
+
: { Array: expr.elements.map((e) => lowerBoundValue(e, targetVar)) };
|
|
1202
|
+
case 'ObjectLiteral':
|
|
1203
|
+
return isFullyLiteral(expr)
|
|
1204
|
+
? { Value: lowerKipValue(expr) }
|
|
1205
|
+
: {
|
|
1206
|
+
Object: expr.entries.map((e) => [e.key, lowerBoundValue(e.value, targetVar)])
|
|
1207
|
+
};
|
|
1208
|
+
default:
|
|
1209
|
+
return { Value: lowerKipValue(expr) };
|
|
578
1210
|
}
|
|
579
1211
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1212
|
+
/** True when nothing in the subtree needs binding at execution time. */
|
|
1213
|
+
function isFullyLiteral(expr) {
|
|
1214
|
+
switch (expr.kind) {
|
|
1215
|
+
case 'StringLiteral':
|
|
1216
|
+
case 'NumberLiteral':
|
|
1217
|
+
case 'BooleanLiteral':
|
|
1218
|
+
case 'NullLiteral':
|
|
1219
|
+
return true;
|
|
1220
|
+
case 'ArrayLiteral':
|
|
1221
|
+
return expr.elements.every(isFullyLiteral);
|
|
1222
|
+
case 'ObjectLiteral':
|
|
1223
|
+
return expr.entries.every((e) => isFullyLiteral(e.value));
|
|
1224
|
+
case 'UnaryExpression':
|
|
1225
|
+
return expr.operator === '-' && isFullyLiteral(expr.operand);
|
|
1226
|
+
default:
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
586
1229
|
}
|
|
587
|
-
function
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
};
|
|
599
|
-
case 'METADATA':
|
|
1230
|
+
function lowerUpdateExpr(expr, targetVar) {
|
|
1231
|
+
switch (expr.kind) {
|
|
1232
|
+
case 'FunctionCallExpr': {
|
|
1233
|
+
const func = UPDATE_FUNCTIONS.get(expr.name.toUpperCase());
|
|
1234
|
+
if (!func) {
|
|
1235
|
+
throw invalidSyntax(`${expr.name} is not a KIP update function; expected ADD, MUL, CLAMP or COALESCE`, expr.range);
|
|
1236
|
+
}
|
|
1237
|
+
const arity = UPDATE_ARITY[func];
|
|
1238
|
+
if (expr.args.length !== arity) {
|
|
1239
|
+
throw invalidSyntax(`${expr.name} takes ${arity} arguments, found ${expr.args.length}`, expr.range);
|
|
1240
|
+
}
|
|
600
1241
|
return {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
where_clauses
|
|
1242
|
+
Function: {
|
|
1243
|
+
func,
|
|
1244
|
+
args: expr.args.map((arg) => lowerUpdateExpr(arg, targetVar))
|
|
605
1245
|
}
|
|
606
1246
|
};
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1247
|
+
}
|
|
1248
|
+
case 'ParameterRef':
|
|
1249
|
+
return { Param: paramName(expr.name) };
|
|
1250
|
+
case 'NumberLiteral':
|
|
1251
|
+
return { Number: numberValue(expr) };
|
|
1252
|
+
case 'UnaryExpression':
|
|
1253
|
+
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1254
|
+
return { Number: -numberValue(expr.operand) };
|
|
613
1255
|
}
|
|
614
|
-
|
|
1256
|
+
throw invalidSyntax(`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`, expr.range);
|
|
1257
|
+
case 'VariableRef':
|
|
1258
|
+
case 'FieldAccess': {
|
|
1259
|
+
const path = lowerDotPath(expr);
|
|
1260
|
+
guardOwnField(path, targetVar, expr.range);
|
|
1261
|
+
return { Variable: path };
|
|
1262
|
+
}
|
|
1263
|
+
default:
|
|
1264
|
+
throw invalidSyntax(`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`, expr.range);
|
|
615
1265
|
}
|
|
616
1266
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* An update expression may read only the element being updated.
|
|
1269
|
+
*
|
|
1270
|
+
* Reading another variable would make the result depend on a join the
|
|
1271
|
+
* statement never declared, so each matched element must be computable from
|
|
1272
|
+
* its own row.
|
|
1273
|
+
*/
|
|
1274
|
+
function guardOwnField(path, targetVar, range) {
|
|
1275
|
+
if (targetVar !== null && path.var !== targetVar) {
|
|
1276
|
+
throw invalidSyntax(`an update expression may read only the target ?${targetVar}, found ?${path.var}`, range);
|
|
620
1277
|
}
|
|
621
|
-
return keys;
|
|
622
1278
|
}
|
|
623
1279
|
// ---------------------------------------------------------------------------
|
|
624
1280
|
// META
|
|
625
1281
|
// ---------------------------------------------------------------------------
|
|
626
|
-
function
|
|
627
|
-
switch (stmt.
|
|
628
|
-
case '
|
|
629
|
-
return
|
|
630
|
-
case '
|
|
631
|
-
return
|
|
632
|
-
case '
|
|
1282
|
+
function lowerMeta(stmt) {
|
|
1283
|
+
switch (stmt.kind) {
|
|
1284
|
+
case 'DescribeStatement':
|
|
1285
|
+
return { Describe: lowerDescribe(stmt) };
|
|
1286
|
+
case 'ListStatement':
|
|
1287
|
+
return { List: lowerList(stmt) };
|
|
1288
|
+
case 'SearchStatement':
|
|
1289
|
+
return { Search: lowerSearch(stmt) };
|
|
1290
|
+
case 'VerifyStatement':
|
|
633
1291
|
return {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1292
|
+
Verify: {
|
|
1293
|
+
target: VERIFY_TARGETS[stmt.target],
|
|
1294
|
+
value: lowerScalar(stmt.value)
|
|
637
1295
|
}
|
|
638
1296
|
};
|
|
639
|
-
case '
|
|
1297
|
+
case 'ValidateStatement':
|
|
640
1298
|
return {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1299
|
+
Validate: {
|
|
1300
|
+
target: VALIDATE_TARGETS[stmt.target],
|
|
1301
|
+
value: lowerScalar(stmt.value),
|
|
1302
|
+
options: stmt.options ? lowerBoundObject(stmt.options) : null
|
|
644
1303
|
}
|
|
645
1304
|
};
|
|
646
|
-
case '
|
|
647
|
-
return {
|
|
648
|
-
|
|
649
|
-
|
|
1305
|
+
case 'PreviewStatement':
|
|
1306
|
+
return {
|
|
1307
|
+
Preview: stmt.target === 'KML'
|
|
1308
|
+
? { Kml: lowerScalar(stmt.value) }
|
|
1309
|
+
: {
|
|
1310
|
+
ImportCapsule: {
|
|
1311
|
+
capsule: lowerScalar(stmt.value),
|
|
1312
|
+
into: lowerScalar(stmt.into)
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
case 'HistoryStatement':
|
|
1317
|
+
return { History: lowerHistory(stmt) };
|
|
1318
|
+
case 'ChangesStatement':
|
|
1319
|
+
return {
|
|
1320
|
+
Changes: stmt.mode === 'SINCE'
|
|
1321
|
+
? {
|
|
1322
|
+
Since: {
|
|
1323
|
+
cursor: lowerScalar(stmt.value),
|
|
1324
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
: {
|
|
1328
|
+
AfterSeq: {
|
|
1329
|
+
seq: lowerScalar(stmt.value),
|
|
1330
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
case 'SnapshotStatement':
|
|
1335
|
+
return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } };
|
|
1336
|
+
case 'ExportCapsuleStatement':
|
|
1337
|
+
return { ExportCapsule: lowerExport(stmt) };
|
|
1338
|
+
default:
|
|
1339
|
+
throw invalidSyntax(`${stmt.kind} is not an executable KIP command`, stmt.range);
|
|
650
1340
|
}
|
|
651
1341
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1342
|
+
const VERIFY_TARGETS = {
|
|
1343
|
+
CAPSULE: 'Capsule',
|
|
1344
|
+
SCHEMA_PACKAGE: 'SchemaPackage',
|
|
1345
|
+
RECEIPT: 'Receipt',
|
|
1346
|
+
BLOB: 'Blob',
|
|
1347
|
+
CHECKPOINT: 'Checkpoint'
|
|
1348
|
+
};
|
|
1349
|
+
const VALIDATE_TARGETS = {
|
|
1350
|
+
KQL: 'Kql',
|
|
1351
|
+
KML: 'Kml',
|
|
1352
|
+
CAPSULE: 'Capsule',
|
|
1353
|
+
SCHEMA_PACKAGE: 'SchemaPackage',
|
|
1354
|
+
IMPORT_PLAN: 'ImportPlan'
|
|
1355
|
+
};
|
|
1356
|
+
const LIST_TARGETS = {
|
|
1357
|
+
SPACES: 'Spaces',
|
|
1358
|
+
SCHEMA_PACKAGES: 'SchemaPackages',
|
|
1359
|
+
TYPES: 'Types',
|
|
1360
|
+
PREDICATES: 'Predicates',
|
|
1361
|
+
FACETS: 'Facets',
|
|
1362
|
+
STRUCTURAL_FIELDS: 'StructuralFields',
|
|
1363
|
+
EPISTEMIC_POLICIES: 'EpistemicPolicies'
|
|
1364
|
+
};
|
|
1365
|
+
const SEARCH_TARGETS = {
|
|
1366
|
+
CONCEPT: 'Concept',
|
|
1367
|
+
PROPOSITION: 'Proposition',
|
|
1368
|
+
ASSERTION: 'Assertion',
|
|
1369
|
+
EVIDENCE: 'Evidence',
|
|
1370
|
+
ACTIVITY: 'Activity',
|
|
1371
|
+
COGNITION: 'Cognition'
|
|
1372
|
+
};
|
|
1373
|
+
function lowerDescribe(stmt) {
|
|
1374
|
+
const value = () => {
|
|
1375
|
+
if (!stmt.value) {
|
|
1376
|
+
throw invalidSyntax(`DESCRIBE ${stmt.target} requires an operand`, stmt.range);
|
|
1377
|
+
}
|
|
1378
|
+
return lowerScalar(stmt.value);
|
|
1379
|
+
};
|
|
1380
|
+
switch (stmt.target) {
|
|
1381
|
+
case 'PRIMER':
|
|
1382
|
+
return { Primer: { mode: stmt.mode ? lowerScalar(stmt.mode) : null } };
|
|
1383
|
+
case 'PROTOCOL':
|
|
1384
|
+
return 'Protocol';
|
|
1385
|
+
case 'EXECUTION_CONTEXT':
|
|
1386
|
+
return 'ExecutionContext';
|
|
1387
|
+
case 'CAPABILITIES':
|
|
1388
|
+
return 'Capabilities';
|
|
1389
|
+
case 'SPACE':
|
|
1390
|
+
return { Space: { value: stmt.value ? lowerScalar(stmt.value) : null } };
|
|
1391
|
+
case 'SCHEMA_ENVIRONMENT':
|
|
1392
|
+
return {
|
|
1393
|
+
SchemaEnvironment: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null }
|
|
1394
|
+
};
|
|
1395
|
+
case 'PACKAGE':
|
|
1396
|
+
return { Package: value() };
|
|
1397
|
+
case 'TYPE':
|
|
1398
|
+
return { Type: value() };
|
|
1399
|
+
case 'PREDICATE':
|
|
1400
|
+
return { Predicate: value() };
|
|
1401
|
+
case 'FACET':
|
|
1402
|
+
return { Facet: value() };
|
|
1403
|
+
case 'STRUCTURAL_FIELD':
|
|
1404
|
+
return { StructuralField: value() };
|
|
1405
|
+
case 'COMPATIBILITY':
|
|
1406
|
+
if (!stmt.from || !stmt.to) {
|
|
1407
|
+
throw invalidSyntax('DESCRIBE COMPATIBILITY requires FROM and TO', stmt.range);
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
Compatibility: { from: lowerScalar(stmt.from), to: lowerScalar(stmt.to) }
|
|
1411
|
+
};
|
|
1412
|
+
case 'ERROR':
|
|
1413
|
+
return { Error: value() };
|
|
1414
|
+
case 'TRANSACTION':
|
|
1415
|
+
return { Transaction: value() };
|
|
1416
|
+
case 'TRANSACTION_BY_IDEMPOTENCY_KEY':
|
|
1417
|
+
return { TransactionByIdempotencyKey: value() };
|
|
1418
|
+
case 'SNAPSHOT':
|
|
1419
|
+
return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } };
|
|
1420
|
+
case 'CAPSULE':
|
|
1421
|
+
return { Capsule: value() };
|
|
1422
|
+
case 'EPISTEMIC_POLICY':
|
|
1423
|
+
return {
|
|
1424
|
+
EpistemicPolicy: { value: stmt.value ? lowerScalar(stmt.value) : null }
|
|
1425
|
+
};
|
|
1426
|
+
case 'PROJECTION_CAPABILITY':
|
|
1427
|
+
return 'ProjectionCapability';
|
|
1428
|
+
case 'TRUST':
|
|
1429
|
+
return { Trust: { value: stmt.value ? lowerScalar(stmt.value) : null } };
|
|
1430
|
+
case 'ACCESS':
|
|
1431
|
+
return {
|
|
1432
|
+
Access: { with: stmt.with ? lowerBoundObject(stmt.with) : null }
|
|
1433
|
+
};
|
|
659
1434
|
}
|
|
660
|
-
|
|
1435
|
+
}
|
|
1436
|
+
function lowerList(stmt) {
|
|
1437
|
+
return {
|
|
1438
|
+
target: LIST_TARGETS[stmt.target],
|
|
1439
|
+
status: stmt.status ? lowerScalar(stmt.status) : null,
|
|
1440
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1441
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
1442
|
+
};
|
|
661
1443
|
}
|
|
662
1444
|
function lowerSearch(stmt) {
|
|
663
|
-
|
|
664
|
-
target: stmt.
|
|
665
|
-
term:
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1445
|
+
return {
|
|
1446
|
+
target: SEARCH_TARGETS[stmt.searchKind],
|
|
1447
|
+
term: lowerScalar(stmt.term),
|
|
1448
|
+
with_type: stmt.withType ? lowerScalar(stmt.withType) : null,
|
|
1449
|
+
with_predicate: stmt.withPredicate ? lowerScalar(stmt.withPredicate) : null,
|
|
1450
|
+
mode: stmt.mode ? lowerScalar(stmt.mode) : null,
|
|
1451
|
+
threshold: stmt.threshold ? lowerScalar(stmt.threshold) : null,
|
|
1452
|
+
as_of_seq: stmt.asOfSeq ? lowerScalar(stmt.asOfSeq) : null,
|
|
1453
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1454
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
670
1455
|
};
|
|
671
|
-
if (stmt.mode !== undefined) {
|
|
672
|
-
const raw = requireLiteralString(stmt.modeValue, stmt.mode, 'MODE', stmt.range);
|
|
673
|
-
const mode = searchMode(raw);
|
|
674
|
-
if (!mode) {
|
|
675
|
-
throw invalidSyntax(`invalid SEARCH mode: ${JSON.stringify(raw)}, expected "keyword", "semantic", or "hybrid"`, stmt.modeValue?.range ?? stmt.range);
|
|
676
|
-
}
|
|
677
|
-
out.mode = mode;
|
|
678
|
-
}
|
|
679
|
-
if (stmt.threshold) {
|
|
680
|
-
const value = stmt.threshold.value;
|
|
681
|
-
if (value.kind !== 'NumberLiteral') {
|
|
682
|
-
throw invalidSyntax('THRESHOLD takes a number', value.range);
|
|
683
|
-
}
|
|
684
|
-
if (value.value < 0 || value.value > 1) {
|
|
685
|
-
throw invalidSyntax(`THRESHOLD must be between 0.0 and 1.0, got ${value.value}`, value.range);
|
|
686
|
-
}
|
|
687
|
-
// `-0` and `0` are the same threshold; the wire form carries only `0`.
|
|
688
|
-
out.threshold = value.value === 0 ? 0 : value.value;
|
|
689
|
-
}
|
|
690
|
-
return out;
|
|
691
1456
|
}
|
|
692
|
-
function
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return undefined;
|
|
1457
|
+
function lowerHistory(stmt) {
|
|
1458
|
+
const paging = {
|
|
1459
|
+
from_seq: stmt.fromSeq ? lowerScalar(stmt.fromSeq) : null,
|
|
1460
|
+
to_seq: stmt.toSeq ? lowerScalar(stmt.toSeq) : null,
|
|
1461
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1462
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
1463
|
+
};
|
|
1464
|
+
if (stmt.target === 'SPACE') {
|
|
1465
|
+
return { Space: paging };
|
|
702
1466
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (node && node.kind !== 'StringLiteral') {
|
|
706
|
-
throw invalidSyntax(`${what} must be a quoted string`, node.range);
|
|
1467
|
+
if (!stmt.value) {
|
|
1468
|
+
throw invalidSyntax('HISTORY ELEMENT requires an element id', stmt.range);
|
|
707
1469
|
}
|
|
708
|
-
|
|
709
|
-
|
|
1470
|
+
return { Element: { value: lowerScalar(stmt.value), ...paging } };
|
|
1471
|
+
}
|
|
1472
|
+
function lowerExport(stmt) {
|
|
1473
|
+
const where_clauses = lowerWhere(stmt.where);
|
|
1474
|
+
// A Capsule is a bounded, self-contained excerpt. `WHERE { }` selects the
|
|
1475
|
+
// whole Space, which is not a smaller thing to hand somebody — it is the
|
|
1476
|
+
// Brain, exported by accident.
|
|
1477
|
+
if (where_clauses.length === 0) {
|
|
1478
|
+
throw invalidSyntax('expected at least one selection pattern: an unbounded EXPORT is not a Capsule', stmt.where.range);
|
|
710
1479
|
}
|
|
711
|
-
return
|
|
1480
|
+
return {
|
|
1481
|
+
target: lowerElementRef(stmt.target),
|
|
1482
|
+
where_clauses,
|
|
1483
|
+
options: stmt.options ? lowerBoundObject(stmt.options) : null,
|
|
1484
|
+
as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null
|
|
1485
|
+
};
|
|
712
1486
|
}
|
|
713
1487
|
// ---------------------------------------------------------------------------
|
|
714
|
-
//
|
|
1488
|
+
// Leaf conversions
|
|
715
1489
|
// ---------------------------------------------------------------------------
|
|
716
|
-
function
|
|
717
|
-
if (
|
|
718
|
-
return
|
|
719
|
-
const value = limit.value;
|
|
720
|
-
if (value.kind !== 'NumberLiteral' || !isIntegerLiteral(value.raw)) {
|
|
721
|
-
throw invalidSyntax('LIMIT takes a positive integer', value.range);
|
|
1490
|
+
function lowerScalar(value) {
|
|
1491
|
+
if (value.kind === 'ParameterRef') {
|
|
1492
|
+
return { Param: paramName(value.name) };
|
|
722
1493
|
}
|
|
723
|
-
|
|
724
|
-
if (n <= 0n) {
|
|
725
|
-
throw invalidSyntax('LIMIT must be a positive integer (LIMIT 0 is not allowed; omit LIMIT for the engine default)', value.range);
|
|
726
|
-
}
|
|
727
|
-
if (n > BigInt(MAX_LIMIT)) {
|
|
728
|
-
throw invalidSyntax(`LIMIT ${value.raw} is out of range`, value.range);
|
|
729
|
-
}
|
|
730
|
-
return Number(n);
|
|
1494
|
+
return { Literal: lowerKipValue(value) };
|
|
731
1495
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (value.kind !== 'StringLiteral') {
|
|
737
|
-
throw invalidSyntax('CURSOR takes a quoted pagination token', value.range);
|
|
1496
|
+
/** An ASSERT member used where the grammar needs a scalar, e.g. `key:`. */
|
|
1497
|
+
function lowerScalarExpression(expr) {
|
|
1498
|
+
if (expr.kind === 'ParameterRef') {
|
|
1499
|
+
return { Param: paramName(expr.name) };
|
|
738
1500
|
}
|
|
739
|
-
if (
|
|
740
|
-
|
|
1501
|
+
if (expr.kind === 'StringLiteral' ||
|
|
1502
|
+
expr.kind === 'NumberLiteral' ||
|
|
1503
|
+
expr.kind === 'BooleanLiteral' ||
|
|
1504
|
+
expr.kind === 'NullLiteral') {
|
|
1505
|
+
return { Literal: lowerKipValue(expr) };
|
|
741
1506
|
}
|
|
742
|
-
|
|
1507
|
+
throw invalidSyntax(`expected a literal or :parameter, found ${describeExpression(expr)}`, expr.range);
|
|
743
1508
|
}
|
|
744
|
-
function
|
|
745
|
-
return
|
|
1509
|
+
function lowerSymbol(symbol) {
|
|
1510
|
+
return symbol.kind === 'ParameterRef'
|
|
1511
|
+
? { Param: paramName(symbol.name) }
|
|
1512
|
+
: { Name: symbol.parsed };
|
|
746
1513
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
out[entry.key] = lowerJson(entry.value);
|
|
754
|
-
}
|
|
755
|
-
return out;
|
|
756
|
-
}
|
|
757
|
-
function lowerJson(expr) {
|
|
758
|
-
switch (expr.kind) {
|
|
1514
|
+
function lowerElementRef(ref) {
|
|
1515
|
+
switch (ref.kind) {
|
|
1516
|
+
case 'VariableRef':
|
|
1517
|
+
return { Handle: varName(ref.name, ref.range) };
|
|
1518
|
+
case 'ParameterRef':
|
|
1519
|
+
return { Param: paramName(ref.name) };
|
|
759
1520
|
case 'StringLiteral':
|
|
760
|
-
return
|
|
761
|
-
case 'NumberLiteral':
|
|
762
|
-
return numberValue(expr.value, expr.raw, expr.range);
|
|
763
|
-
case 'BooleanLiteral':
|
|
764
|
-
return expr.value;
|
|
765
|
-
case 'NullLiteral':
|
|
766
|
-
return null;
|
|
767
|
-
case 'ArrayLiteral':
|
|
768
|
-
return expr.elements.map(lowerJson);
|
|
769
|
-
case 'ObjectLiteral':
|
|
770
|
-
return lowerJsonEntries(expr.entries);
|
|
771
|
-
default:
|
|
772
|
-
throw invalidSyntax(`expected a JSON value, found ${describeExpression(expr)}`, expr.range);
|
|
1521
|
+
return { Id: ref.parsed };
|
|
773
1522
|
}
|
|
774
1523
|
}
|
|
1524
|
+
// ---------------------------------------------------------------------------
|
|
1525
|
+
// Numeric literals
|
|
1526
|
+
// ---------------------------------------------------------------------------
|
|
1527
|
+
/** `i64::MIN` — the most negative integer a KIP number literal may spell. */
|
|
1528
|
+
const INT_MIN = -(2n ** 63n);
|
|
1529
|
+
/** `u64::MAX` — the largest. */
|
|
1530
|
+
const INT_MAX = 2n ** 64n - 1n;
|
|
1531
|
+
/** An integer literal: no fraction, no exponent, so it is read as an integer. */
|
|
1532
|
+
const INTEGER_FORM = /^-?\d+$/;
|
|
775
1533
|
/**
|
|
776
|
-
*
|
|
1534
|
+
* The value of a number literal, refusing the ones that cannot survive being
|
|
1535
|
+
* one.
|
|
777
1536
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
1537
|
+
* A JavaScript number is a double, so `18446744073709551617` silently becomes
|
|
1538
|
+
* `18446744073709551616` on the way in. Accepting that would be the worst
|
|
1539
|
+
* possible outcome: the command does not fail, it *executes with a different
|
|
1540
|
+
* number than it says*, and no engine downstream can detect it — by the time
|
|
1541
|
+
* an executable AST exists the digits are gone. So the check happens here,
|
|
1542
|
+
* against the raw text, which is the only place the original is still around.
|
|
1543
|
+
*
|
|
1544
|
+
* The bounds are the reference grammar's: an integer literal is read as an
|
|
1545
|
+
* `i64` or a `u64` and must fit one of them, and any other form must parse to a
|
|
1546
|
+
* finite double. `18446744073709551616.0` is therefore accepted where
|
|
1547
|
+
* `18446744073709551616` is not — the float form is claiming an approximation,
|
|
1548
|
+
* and the integer form is claiming an exact value it cannot deliver.
|
|
1549
|
+
*
|
|
1550
|
+
* Integers above 2^53 still lose precision in this implementation's `value`
|
|
1551
|
+
* even though they are accepted, because a double cannot hold them. That is a
|
|
1552
|
+
* property of the host, not a disagreement about the language: both engines
|
|
1553
|
+
* agree the command is legal, and a runtime that needs the exact digits has
|
|
1554
|
+
* `raw`.
|
|
781
1555
|
*/
|
|
782
|
-
function
|
|
783
|
-
if ((
|
|
784
|
-
|
|
785
|
-
|
|
1556
|
+
function numberValue(node) {
|
|
1557
|
+
if (INTEGER_FORM.test(node.raw)) {
|
|
1558
|
+
const exact = BigInt(node.raw);
|
|
1559
|
+
if (exact < INT_MIN || exact > INT_MAX) {
|
|
1560
|
+
throw invalidSyntax(`${node.raw} is outside the range a KIP integer literal can represent ` +
|
|
1561
|
+
`(${INT_MIN} to ${INT_MAX})`, node.range);
|
|
1562
|
+
}
|
|
1563
|
+
return node.value;
|
|
1564
|
+
}
|
|
1565
|
+
if (!Number.isFinite(node.value)) {
|
|
1566
|
+
throw invalidSyntax(`only finite numbers are valid KIP literals, found ${node.raw}`, node.range);
|
|
786
1567
|
}
|
|
1568
|
+
return node.value;
|
|
1569
|
+
}
|
|
1570
|
+
function lowerKipValue(expr) {
|
|
787
1571
|
switch (expr.kind) {
|
|
788
1572
|
case 'StringLiteral':
|
|
789
1573
|
return { String: expr.parsed };
|
|
790
1574
|
case 'NumberLiteral':
|
|
791
|
-
return { Number: numberValue(expr
|
|
1575
|
+
return { Number: numberValue(expr) };
|
|
792
1576
|
case 'BooleanLiteral':
|
|
793
1577
|
return { Bool: expr.value };
|
|
794
1578
|
case 'NullLiteral':
|
|
795
1579
|
return 'Null';
|
|
796
1580
|
case 'ArrayLiteral':
|
|
797
1581
|
return { Array: expr.elements.map(lowerKipValue) };
|
|
798
|
-
case 'ObjectLiteral':
|
|
1582
|
+
case 'ObjectLiteral':
|
|
1583
|
+
case 'ObjectPattern': {
|
|
1584
|
+
const entries = expr.kind === 'ObjectLiteral' ? expr.entries : expr.members;
|
|
799
1585
|
const out = {};
|
|
800
|
-
for (const entry of
|
|
801
|
-
if (Object.prototype.hasOwnProperty.call(out, entry.key)) {
|
|
802
|
-
throw invalidSyntax(`duplicate key in object (keys must be unique): ${entry.key}`, entry.range);
|
|
803
|
-
}
|
|
1586
|
+
for (const entry of entries) {
|
|
804
1587
|
out[entry.key] = lowerKipValue(entry.value);
|
|
805
1588
|
}
|
|
806
1589
|
return { Object: out };
|
|
807
1590
|
}
|
|
1591
|
+
case 'UnaryExpression':
|
|
1592
|
+
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1593
|
+
return { Number: -numberValue(expr.operand) };
|
|
1594
|
+
}
|
|
1595
|
+
throw invalidSyntax(`expected a value, found ${describeExpression(expr)}`, expr.range);
|
|
808
1596
|
default:
|
|
809
|
-
throw invalidSyntax(`expected a
|
|
1597
|
+
throw invalidSyntax(`expected a value, found ${describeExpression(expr)}`, expr.range);
|
|
810
1598
|
}
|
|
811
1599
|
}
|
|
812
1600
|
/**
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
* `1.8446744073709552e19`.
|
|
816
|
-
*
|
|
817
|
-
* Known limit: this tree carries numbers as JavaScript `number`, so integers
|
|
818
|
-
* above 2^53 still lose precision here even though an i64/u64 engine keeps
|
|
819
|
-
* them — `9007199254740993` lowers to `9007199254740992`. Closing that gap
|
|
820
|
-
* needs a bigint-carrying wire type, not a wider bound.
|
|
1601
|
+
* Option and epistemic blocks are `data_value`s, not plain JSON: the grammar
|
|
1602
|
+
* lets a parameter stand anywhere inside them.
|
|
821
1603
|
*/
|
|
822
|
-
function
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
throw invalidSyntax(`invalid number literal ${raw}`, range);
|
|
827
|
-
}
|
|
828
|
-
if (!isIntegerLiteral(raw)) {
|
|
829
|
-
if (!Number.isFinite(value)) {
|
|
830
|
-
throw invalidSyntax(`number literal ${raw} is out of range`, range);
|
|
831
|
-
}
|
|
832
|
-
return value;
|
|
833
|
-
}
|
|
834
|
-
const n = BigInt(raw);
|
|
835
|
-
if (n < I64_MIN || n > U64_MAX) {
|
|
836
|
-
throw invalidSyntax(`integer literal ${raw} is out of range: KIP integers must be representable as i64 or u64`, range);
|
|
837
|
-
}
|
|
838
|
-
// `-0` is an integer literal, and the wire format carries it as `0`.
|
|
839
|
-
return n === 0n ? 0 : Number(n);
|
|
840
|
-
}
|
|
841
|
-
const JSON_NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/;
|
|
842
|
-
function isIntegerLiteral(raw) {
|
|
843
|
-
return !/[.eE]/.test(raw);
|
|
844
|
-
}
|
|
845
|
-
/** Reads `?var`, `?var.field` or `?var.attributes.key` as a name plus a path. */
|
|
846
|
-
function dotPathVar(expr) {
|
|
847
|
-
const path = [];
|
|
848
|
-
let node = expr;
|
|
849
|
-
while (node.kind === 'DotExpression') {
|
|
850
|
-
path.unshift(node.property);
|
|
851
|
-
node = node.object;
|
|
852
|
-
}
|
|
853
|
-
if (node.kind !== 'VariableRef') {
|
|
854
|
-
throw invalidSyntax(`expected a variable, found ${describeExpression(node)}`, node.range);
|
|
1604
|
+
function lowerBoundObject(object) {
|
|
1605
|
+
const out = {};
|
|
1606
|
+
for (const entry of object.entries) {
|
|
1607
|
+
out[entry.key] = lowerBoundValue(entry.value, null);
|
|
855
1608
|
}
|
|
856
|
-
return
|
|
1609
|
+
return out;
|
|
857
1610
|
}
|
|
858
1611
|
/** Strips the `?` sigil; the executable form carries bare names. */
|
|
859
1612
|
function varName(name, range) {
|
|
@@ -862,14 +1615,22 @@ function varName(name, range) {
|
|
|
862
1615
|
}
|
|
863
1616
|
return name.slice(1);
|
|
864
1617
|
}
|
|
1618
|
+
/** Strips the `:` sigil; the executable form carries bare names. */
|
|
1619
|
+
function paramName(name) {
|
|
1620
|
+
return name.startsWith(':') ? name.slice(1) : name;
|
|
1621
|
+
}
|
|
865
1622
|
function describeExpression(expr) {
|
|
866
1623
|
switch (expr.kind) {
|
|
867
1624
|
case 'ParameterRef':
|
|
868
|
-
return `the parameter ${expr.name}
|
|
1625
|
+
return `the parameter ${expr.name}`;
|
|
869
1626
|
case 'VariableRef':
|
|
870
1627
|
return `the variable ${expr.name}`;
|
|
871
1628
|
case 'FunctionCallExpr':
|
|
872
1629
|
return `a call to ${expr.name}`;
|
|
1630
|
+
case 'AggregateExpr':
|
|
1631
|
+
return `the aggregate ${expr.name}`;
|
|
1632
|
+
case 'BinaryExpression':
|
|
1633
|
+
return `the operator ${expr.operator}`;
|
|
873
1634
|
default:
|
|
874
1635
|
return expr.kind;
|
|
875
1636
|
}
|