@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/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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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);
|
|
352
|
+
}
|
|
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);
|
|
289
357
|
}
|
|
290
|
-
if (
|
|
291
|
-
throw invalidSyntax(
|
|
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) };
|
|
303
386
|
case 'VariableRef':
|
|
304
|
-
return { Variable: varName(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return { Proposition: lowerPropositionMatcher(
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
case '
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
387
|
+
return { Variable: varName(atom.name, atom.range) };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function lowerTerm(term) {
|
|
391
|
+
switch (term.kind) {
|
|
392
|
+
case 'VariableRef':
|
|
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,1074 @@ 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);
|
|
408
525
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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);
|
|
412
544
|
}
|
|
413
|
-
|
|
545
|
+
seen.add(handle);
|
|
414
546
|
}
|
|
415
|
-
return { Literal: lowerKipValue(expr) };
|
|
416
547
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
573
|
+
}
|
|
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
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
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
|
-
function
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
736
|
+
function lowerUpsertConcept(stmt) {
|
|
737
|
+
const match = stmt.match ? lowerObjectMatcher(stmt.match.pattern) : null;
|
|
738
|
+
// Identity for an upsert is `id` or `key`; a name-only match is forbidden
|
|
739
|
+
// because names are mutable grounding state with duplicates allowed, so
|
|
740
|
+
// "the Concept named X" can silently address a different node over time.
|
|
741
|
+
if (match) {
|
|
742
|
+
const fields = Object.keys(match);
|
|
743
|
+
const hasIdentity = fields.includes('id') || fields.includes('key');
|
|
744
|
+
if (!hasIdentity) {
|
|
745
|
+
throw invalidSyntax('UPSERT CONCEPT must match on a stable identity: add {id: ...} or {key: ...} — ' +
|
|
746
|
+
'name is mutable grounding state and never identifies a Concept', stmt.match.range);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
handle: varName(stmt.handle.name, stmt.handle.range),
|
|
751
|
+
match,
|
|
752
|
+
expect_version: stmt.expectVersion
|
|
753
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
754
|
+
: null,
|
|
755
|
+
set_fields: stmt.setFields
|
|
756
|
+
? lowerAssignments(stmt.setFields.assignments, null)
|
|
757
|
+
: null,
|
|
758
|
+
set_attributes: stmt.setAttributes
|
|
759
|
+
? lowerAssignments(stmt.setAttributes.assignments, null)
|
|
458
760
|
: null,
|
|
459
|
-
|
|
761
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
762
|
+
unset_attributes: stmt.unsetAttributes
|
|
763
|
+
? lowerUnsetFields(stmt.unsetAttributes.fields)
|
|
764
|
+
: null,
|
|
765
|
+
unset_facets: stmt.unsetFacets.map((f) => ({
|
|
766
|
+
facet: lowerSymbol(f.facet),
|
|
767
|
+
fields: lowerUnsetFields(f.fields)
|
|
768
|
+
})),
|
|
769
|
+
set_structural: stmt.setStructural
|
|
770
|
+
? lowerStructural(stmt.setStructural, null)
|
|
771
|
+
: null,
|
|
772
|
+
unset_structural: stmt.unsetStructural
|
|
773
|
+
? lowerStructuralRemovals(stmt.unsetStructural, null)
|
|
774
|
+
: null
|
|
460
775
|
};
|
|
461
|
-
const version = lowerExpectVersion(block);
|
|
462
|
-
if (version !== undefined)
|
|
463
|
-
out.expect_version = version;
|
|
464
|
-
return out;
|
|
465
776
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
777
|
+
function lowerEnsureProposition(stmt) {
|
|
778
|
+
const triple = requireStructuralTuple(stmt.tuple, 'ENSURE PROPOSITION');
|
|
779
|
+
return {
|
|
780
|
+
handle: stmt.handle ? varName(stmt.handle.name, stmt.handle.range) : null,
|
|
781
|
+
...triple,
|
|
782
|
+
expect_version: stmt.expectVersion
|
|
783
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
784
|
+
: null
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function lowerRecordCreate(stmt) {
|
|
788
|
+
const fields = stmt.setFields
|
|
789
|
+
? lowerAssignments(stmt.setFields.assignments, null)
|
|
790
|
+
: null;
|
|
791
|
+
return {
|
|
792
|
+
handle: varName(stmt.handle.name, stmt.handle.range),
|
|
793
|
+
client_key: stmt.clientKey ? lowerScalar(stmt.clientKey.value) : null,
|
|
794
|
+
set_fields: fields,
|
|
795
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
796
|
+
set_structural: stmt.setStructural
|
|
797
|
+
? lowerStructural(stmt.setStructural, null)
|
|
798
|
+
: null
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Desugars `ASSERT` into exactly what the Spec defines it as (§55.1):
|
|
803
|
+
* `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `SUPERSEDE` when written.
|
|
804
|
+
*
|
|
805
|
+
* Nothing else is fabricated. The sugar exists because recording an
|
|
806
|
+
* attributed claim is the hot path, not because it means anything new.
|
|
807
|
+
*/
|
|
808
|
+
function lowerAssertSugar(stmt, seq) {
|
|
809
|
+
const members = new Map();
|
|
810
|
+
for (const entry of stmt.assignments.entries) {
|
|
811
|
+
if (members.has(entry.key)) {
|
|
812
|
+
throw invalidSyntax(`duplicate ASSERT member ${entry.key}`, entry.range);
|
|
813
|
+
}
|
|
814
|
+
members.set(entry.key, entry.value);
|
|
815
|
+
}
|
|
816
|
+
const known = new Set([
|
|
817
|
+
'by',
|
|
818
|
+
'mode',
|
|
819
|
+
'stance',
|
|
820
|
+
'confidence',
|
|
821
|
+
'at',
|
|
822
|
+
'valid',
|
|
823
|
+
'evidence',
|
|
824
|
+
'key'
|
|
825
|
+
]);
|
|
826
|
+
for (const [key, value] of members) {
|
|
827
|
+
if (!known.has(key)) {
|
|
828
|
+
throw invalidSyntax(`${key} is not an ASSERT member; expected one of ${[...known].join(', ')}`, value.range);
|
|
829
|
+
}
|
|
472
830
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
831
|
+
// `by` names whose stance this is, and `mode` says how it was arrived at.
|
|
832
|
+
// Neither has a safe default: guessing the actor would forge attribution,
|
|
833
|
+
// and guessing the mode would turn hearsay into observation.
|
|
834
|
+
const by = members.get('by');
|
|
835
|
+
if (!by) {
|
|
836
|
+
throw invalidSyntax('ASSERT requires by: <semantic actor> — an Assertion without an assertor has no epistemic owner', stmt.assignments.range);
|
|
476
837
|
}
|
|
477
|
-
|
|
838
|
+
const mode = members.get('mode');
|
|
839
|
+
if (!mode) {
|
|
840
|
+
throw invalidSyntax('ASSERT requires mode: one of observed, stated, inferred, predicted, hypothetical, imported', stmt.assignments.range);
|
|
841
|
+
}
|
|
842
|
+
// The Proposition handle is synthesized, so it must collide with neither a
|
|
843
|
+
// user handle nor another ASSERT in the same plan. `#` cannot occur in a KIP
|
|
844
|
+
// identifier, which rules out the first; `seq` is the clause position, which
|
|
845
|
+
// rules out the second — two handle-less ASSERTs in one MUTATE are ordinary
|
|
846
|
+
// input, not a name clash.
|
|
847
|
+
const assertionHandle = stmt.handle
|
|
848
|
+
? varName(stmt.handle.name, stmt.handle.range)
|
|
849
|
+
: `#assert${seq}`;
|
|
850
|
+
const propositionHandle = `${assertionHandle}#proposition`;
|
|
851
|
+
const triple = requireStructuralTuple(stmt.tuple, 'ASSERT');
|
|
852
|
+
const clauses = [
|
|
853
|
+
{
|
|
854
|
+
EnsureProposition: {
|
|
855
|
+
handle: propositionHandle,
|
|
856
|
+
...triple,
|
|
857
|
+
expect_version: null
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
];
|
|
861
|
+
const fields = [
|
|
862
|
+
['proposition', { Handle: propositionHandle }],
|
|
863
|
+
['asserted_by', lowerMutationValue(by, null)],
|
|
864
|
+
['mode', lowerMutationValue(mode, null)],
|
|
865
|
+
// The normative expansion carries a stance even when the source omitted
|
|
866
|
+
// one, so the default is materialized here rather than left for the
|
|
867
|
+
// engine to re-derive.
|
|
868
|
+
[
|
|
869
|
+
'stance',
|
|
870
|
+
members.has('stance')
|
|
871
|
+
? lowerMutationValue(members.get('stance'), null)
|
|
872
|
+
: { Value: { String: 'support' } }
|
|
873
|
+
]
|
|
874
|
+
];
|
|
875
|
+
const optional = [
|
|
876
|
+
['confidence', 'confidence'],
|
|
877
|
+
['at', 'asserted_at'],
|
|
878
|
+
['valid', 'valid_time']
|
|
879
|
+
];
|
|
880
|
+
for (const [member, field] of optional) {
|
|
881
|
+
const value = members.get(member);
|
|
882
|
+
if (value)
|
|
883
|
+
fields.push([field, lowerMutationValue(value, null)]);
|
|
884
|
+
}
|
|
885
|
+
// `evidence` is a reserved Core *structural* field, not a plain one: the
|
|
886
|
+
// normative desugaring emits `("evidence", ref) {role: "support"}`. An array
|
|
887
|
+
// cites several artifacts, so it becomes one role-qualified edge each.
|
|
888
|
+
const evidenceExpr = members.get('evidence');
|
|
889
|
+
const evidenceEdges = evidenceExpr === undefined
|
|
890
|
+
? []
|
|
891
|
+
: (evidenceExpr.kind === 'ArrayLiteral'
|
|
892
|
+
? evidenceExpr.elements
|
|
893
|
+
: [evidenceExpr]).map((ref) => ({
|
|
894
|
+
field: { Name: 'evidence' },
|
|
895
|
+
value: lowerMutationValue(ref, null),
|
|
896
|
+
options: { role: { Value: { String: 'support' } } }
|
|
897
|
+
}));
|
|
898
|
+
const clientKeyExpr = members.get('key');
|
|
899
|
+
clauses.push({
|
|
900
|
+
CreateAssertion: {
|
|
901
|
+
handle: assertionHandle,
|
|
902
|
+
client_key: clientKeyExpr ? lowerScalarExpression(clientKeyExpr) : null,
|
|
903
|
+
set_fields: fields,
|
|
904
|
+
set_facets: [],
|
|
905
|
+
set_structural: evidenceEdges.length > 0 ? evidenceEdges : null
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
if (stmt.superseding) {
|
|
909
|
+
clauses.push({
|
|
910
|
+
SupersedeAssertion: {
|
|
911
|
+
target: lowerElementRef(stmt.superseding),
|
|
912
|
+
by: { Handle: assertionHandle },
|
|
913
|
+
expect_state: null
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return clauses;
|
|
478
918
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
919
|
+
function lowerTransition(stmt) {
|
|
920
|
+
let setFields = null;
|
|
921
|
+
let setStructural = null;
|
|
922
|
+
for (const clause of stmt.finalize) {
|
|
923
|
+
if (clause.kind === 'SetFieldsClause') {
|
|
924
|
+
if (setFields) {
|
|
925
|
+
throw invalidSyntax('duplicate SET FIELDS clause', clause.range);
|
|
926
|
+
}
|
|
927
|
+
setFields = lowerAssignments(clause.assignments, null);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
if (setStructural) {
|
|
931
|
+
throw invalidSyntax('duplicate SET STRUCTURAL clause', clause.range);
|
|
932
|
+
}
|
|
933
|
+
setStructural = lowerStructural(clause, null);
|
|
934
|
+
}
|
|
486
935
|
}
|
|
487
|
-
|
|
488
|
-
|
|
936
|
+
return {
|
|
937
|
+
target: lowerElementRef(stmt.target),
|
|
938
|
+
to: lowerScalar(stmt.to),
|
|
939
|
+
set_fields: setFields,
|
|
940
|
+
set_structural: setStructural,
|
|
941
|
+
expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
|
|
942
|
+
};
|
|
489
943
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
944
|
+
function lowerRemoval(stmt) {
|
|
945
|
+
return {
|
|
946
|
+
target: lowerElementRef(stmt.target),
|
|
947
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
948
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
949
|
+
expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function lowerPurge(stmt) {
|
|
953
|
+
if (stmt.confirm.parsed !== 'PURGE') {
|
|
954
|
+
throw invalidSyntax('PURGE must be confirmed with the exact literal "PURGE"', stmt.confirm.range);
|
|
495
955
|
}
|
|
496
|
-
|
|
497
|
-
|
|
956
|
+
return {
|
|
957
|
+
target: lowerElementRef(stmt.target),
|
|
958
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
959
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
960
|
+
reference_policy: stmt.referencePolicy
|
|
961
|
+
? lowerScalar(stmt.referencePolicy)
|
|
962
|
+
: null,
|
|
963
|
+
confirm: 'PURGE'
|
|
964
|
+
};
|
|
498
965
|
}
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
// UPDATE
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
499
969
|
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);
|
|
970
|
+
if (stmt.actions.length === 0) {
|
|
971
|
+
throw invalidSyntax('UPDATE requires at least one SET or UNSET action', stmt.range);
|
|
505
972
|
}
|
|
973
|
+
const target = lowerElementRef(stmt.target);
|
|
974
|
+
const targetVar = 'Handle' in target ? target.Handle : null;
|
|
975
|
+
const kind = targetVar && stmt.where ? boundKindOf(targetVar, stmt.where.patterns) : null;
|
|
976
|
+
const actions = stmt.actions.map((action) => lowerUpdateAction(action, targetVar, kind));
|
|
506
977
|
return {
|
|
507
978
|
target,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
979
|
+
expect_version: stmt.expectVersion
|
|
980
|
+
? lowerScalar(stmt.expectVersion.value)
|
|
981
|
+
: null,
|
|
982
|
+
actions,
|
|
983
|
+
where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
|
|
984
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
512
985
|
};
|
|
513
986
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
987
|
+
function boundKindOf(variable, patterns) {
|
|
988
|
+
for (const pattern of patterns) {
|
|
989
|
+
switch (pattern.kind) {
|
|
990
|
+
case 'AssertionPattern':
|
|
991
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
992
|
+
return 'assertion';
|
|
993
|
+
}
|
|
994
|
+
break;
|
|
995
|
+
case 'EvidencePattern':
|
|
996
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
997
|
+
return 'evidence';
|
|
998
|
+
}
|
|
999
|
+
break;
|
|
1000
|
+
case 'ActivityPattern':
|
|
1001
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1002
|
+
return 'activity';
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
case 'ConceptPattern':
|
|
1006
|
+
if (varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1007
|
+
return 'concept';
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
1010
|
+
case 'PropositionPattern':
|
|
1011
|
+
if (pattern.variable &&
|
|
1012
|
+
varName(pattern.variable.name, pattern.variable.range) === variable) {
|
|
1013
|
+
return 'proposition';
|
|
1014
|
+
}
|
|
1015
|
+
break;
|
|
1016
|
+
case 'NotClause':
|
|
1017
|
+
case 'OptionalClause':
|
|
1018
|
+
case 'UnionClause': {
|
|
1019
|
+
const nested = boundKindOf(variable, pattern.patterns);
|
|
1020
|
+
if (nested)
|
|
1021
|
+
return nested;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
519
1025
|
}
|
|
520
|
-
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
function lowerUpdateAction(action, targetVar, kind) {
|
|
1029
|
+
switch (action.kind) {
|
|
1030
|
+
case 'SetFieldsClause': {
|
|
1031
|
+
const assignments = lowerAssignments(action.assignments, targetVar);
|
|
1032
|
+
for (const entry of action.assignments.entries) {
|
|
1033
|
+
guardImmutableField(entry.key, kind, entry.range);
|
|
1034
|
+
}
|
|
1035
|
+
return { SetFields: assignments };
|
|
1036
|
+
}
|
|
1037
|
+
case 'SetAttributesClause': {
|
|
1038
|
+
const assignments = lowerAssignments(action.assignments, targetVar);
|
|
1039
|
+
for (const entry of action.assignments.entries) {
|
|
1040
|
+
guardProtectedField(entry.key, entry.range);
|
|
1041
|
+
}
|
|
1042
|
+
return { SetAttributes: assignments };
|
|
1043
|
+
}
|
|
1044
|
+
case 'SetFacetClause':
|
|
1045
|
+
return { SetFacet: lowerFacet(action, targetVar) };
|
|
1046
|
+
case 'UnsetAttributesClause':
|
|
1047
|
+
return { UnsetAttributes: lowerUnsetFields(action.fields) };
|
|
1048
|
+
case 'UnsetFacetClause':
|
|
1049
|
+
return {
|
|
1050
|
+
UnsetFacet: {
|
|
1051
|
+
facet: lowerSymbol(action.facet),
|
|
1052
|
+
fields: lowerUnsetFields(action.fields)
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
case 'SetStructuralClause':
|
|
1056
|
+
guardStructuralMutation('SET STRUCTURAL', kind, action.range);
|
|
1057
|
+
return { SetStructural: lowerStructural(action, targetVar) };
|
|
1058
|
+
case 'UnsetStructuralClause':
|
|
1059
|
+
guardStructuralMutation('UNSET STRUCTURAL', kind, action.range);
|
|
1060
|
+
return { UnsetStructural: lowerStructuralRemovals(action, targetVar) };
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Structural mutation reaches mutable Concept topology only (Spec §17.5).
|
|
1065
|
+
* Record kinds keep their topology: an Assertion's citations and an
|
|
1066
|
+
* Evidence's lineage are immutable payload, a Proposition has no structural
|
|
1067
|
+
* fields, and a pending Activity finalizes through TRANSITION ACTIVITY.
|
|
1068
|
+
*/
|
|
1069
|
+
function guardStructuralMutation(verb, kind, range) {
|
|
1070
|
+
switch (kind) {
|
|
1071
|
+
case 'assertion':
|
|
1072
|
+
throw invalidSyntax(`${verb} cannot change an Assertion's citations: they are immutable payload — record a new Assertion with SUPERSEDING`, range);
|
|
1073
|
+
case 'evidence':
|
|
1074
|
+
throw invalidSyntax(`${verb} cannot change Evidence topology: correct it with CORRECT EVIDENCE :old BY :new`, range);
|
|
1075
|
+
case 'proposition':
|
|
1076
|
+
throw invalidSyntax(`${verb} has no target on a Proposition: a Proposition is its tuple and carries no structural fields`, range);
|
|
1077
|
+
case 'activity':
|
|
1078
|
+
throw invalidSyntax(`${verb} cannot change Activity topology: finalize a pending Activity with TRANSITION ACTIVITY ... SET STRUCTURAL; a terminal Activity is immutable`, range);
|
|
1079
|
+
default:
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/** Engine-owned state is never author-writable, whatever the element kind. */
|
|
1084
|
+
function guardProtectedField(field, range) {
|
|
1085
|
+
if (PROTECTED_FIELDS.has(field)) {
|
|
1086
|
+
throw invalidSyntax(`${field} is engine-maintained state and cannot be written by a mutation`, range);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function guardImmutableField(field, kind, range) {
|
|
1090
|
+
guardProtectedField(field, range);
|
|
1091
|
+
if (kind === 'assertion' && ASSERTION_IMMUTABLE.has(field)) {
|
|
1092
|
+
throw invalidSyntax(`${field} is immutable Assertion payload: record the change as a new Assertion with SUPERSEDING, ` +
|
|
1093
|
+
'never by rewriting the old one', range);
|
|
1094
|
+
}
|
|
1095
|
+
if (kind === 'evidence' && EVIDENCE_IMMUTABLE.has(field)) {
|
|
1096
|
+
throw invalidSyntax(`${field} is immutable Evidence payload: correct it with CORRECT EVIDENCE :old BY :new`, range);
|
|
1097
|
+
}
|
|
1098
|
+
if (kind === 'proposition' && PROPOSITION_IMMUTABLE.has(field)) {
|
|
1099
|
+
throw invalidSyntax(`${field} is part of the immutable Proposition tuple: a different tuple is a different Proposition`, range);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// ---------------------------------------------------------------------------
|
|
1103
|
+
// Assignments, facets, structural edges
|
|
1104
|
+
// ---------------------------------------------------------------------------
|
|
1105
|
+
function lowerAssignments(object, targetVar) {
|
|
521
1106
|
const seen = new Set();
|
|
522
|
-
|
|
1107
|
+
const out = [];
|
|
1108
|
+
for (const entry of object.entries) {
|
|
1109
|
+
guardProtectedField(entry.key, entry.range);
|
|
523
1110
|
if (seen.has(entry.key)) {
|
|
524
|
-
throw invalidSyntax(`duplicate
|
|
1111
|
+
throw invalidSyntax(`duplicate assignment for ${entry.key}`, entry.range);
|
|
525
1112
|
}
|
|
526
1113
|
seen.add(entry.key);
|
|
527
|
-
out.push([entry.key,
|
|
1114
|
+
out.push([entry.key, lowerMutationValue(entry.value, targetVar)]);
|
|
528
1115
|
}
|
|
529
1116
|
return out;
|
|
530
1117
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1118
|
+
function lowerFacet(clause, targetVar) {
|
|
1119
|
+
return {
|
|
1120
|
+
facet: lowerSymbol(clause.facet),
|
|
1121
|
+
values: lowerAssignments(clause.assignments, targetVar)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function lowerStructural(clause, targetVar) {
|
|
1125
|
+
return clause.assignments.map((assignment) => ({
|
|
1126
|
+
field: lowerSymbol(assignment.field),
|
|
1127
|
+
value: lowerMutationValue(assignment.value, targetVar),
|
|
1128
|
+
options: assignment.options ? lowerBoundObject(assignment.options) : null
|
|
1129
|
+
}));
|
|
1130
|
+
}
|
|
1131
|
+
function lowerStructuralRemovals(clause, targetVar) {
|
|
1132
|
+
if (clause.removals.length === 0) {
|
|
1133
|
+
throw invalidSyntax('UNSET STRUCTURAL removes named references; list at least one (field, target)', clause.range);
|
|
536
1134
|
}
|
|
537
|
-
return
|
|
1135
|
+
return clause.removals.map((removal) => ({
|
|
1136
|
+
field: lowerSymbol(removal.field),
|
|
1137
|
+
value: lowerMutationValue(removal.value, targetVar)
|
|
1138
|
+
}));
|
|
538
1139
|
}
|
|
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);
|
|
1140
|
+
function lowerUnsetFields(fields) {
|
|
1141
|
+
const seen = new Set();
|
|
1142
|
+
for (const field of fields) {
|
|
1143
|
+
if (seen.has(field.name)) {
|
|
1144
|
+
throw invalidSyntax(`duplicate field ${field.name}`, field.range);
|
|
548
1145
|
}
|
|
549
|
-
|
|
1146
|
+
guardProtectedField(field.name, field.range);
|
|
1147
|
+
seen.add(field.name);
|
|
550
1148
|
}
|
|
551
|
-
|
|
552
|
-
|
|
1149
|
+
return [...seen];
|
|
1150
|
+
}
|
|
1151
|
+
function lowerMutationValue(expr, targetVar) {
|
|
1152
|
+
if (expr.kind === 'FunctionCallExpr') {
|
|
1153
|
+
return { Expr: lowerUpdateExpr(expr, targetVar) };
|
|
553
1154
|
}
|
|
554
|
-
if (expr.kind === '
|
|
555
|
-
|
|
1155
|
+
if (expr.kind === 'AggregateExpr') {
|
|
1156
|
+
throw invalidSyntax(`${expr.name} is an aggregate and cannot appear in an assignment`, expr.range);
|
|
556
1157
|
}
|
|
557
|
-
|
|
1158
|
+
return lowerBoundValue(expr, targetVar);
|
|
558
1159
|
}
|
|
559
1160
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* join the engine happened to visit, so a bulk UPDATE would stop being
|
|
564
|
-
* deterministic and order-independent.
|
|
1161
|
+
* Lowers a `data_value`, keeping structure only where something still needs
|
|
1162
|
+
* binding. A wholly literal subtree collapses to one `Value`, so an engine
|
|
1163
|
+
* that has nothing to substitute never walks a binding tree.
|
|
565
1164
|
*/
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
checkUpdateExprTargets(arg, target, key);
|
|
1165
|
+
function lowerBoundValue(expr, targetVar) {
|
|
1166
|
+
switch (expr.kind) {
|
|
1167
|
+
case 'ParameterRef':
|
|
1168
|
+
return { Param: paramName(expr.name) };
|
|
1169
|
+
case 'VariableRef':
|
|
1170
|
+
return { Handle: varName(expr.name, expr.range) };
|
|
1171
|
+
case 'FieldAccess': {
|
|
1172
|
+
const path = lowerDotPath(expr);
|
|
1173
|
+
guardOwnField(path, targetVar, expr.range);
|
|
1174
|
+
return { Variable: path };
|
|
577
1175
|
}
|
|
1176
|
+
case 'ArrayLiteral':
|
|
1177
|
+
return isFullyLiteral(expr)
|
|
1178
|
+
? { Value: lowerKipValue(expr) }
|
|
1179
|
+
: { Array: expr.elements.map((e) => lowerBoundValue(e, targetVar)) };
|
|
1180
|
+
case 'ObjectLiteral':
|
|
1181
|
+
return isFullyLiteral(expr)
|
|
1182
|
+
? { Value: lowerKipValue(expr) }
|
|
1183
|
+
: {
|
|
1184
|
+
Object: expr.entries.map((e) => [e.key, lowerBoundValue(e.value, targetVar)])
|
|
1185
|
+
};
|
|
1186
|
+
default:
|
|
1187
|
+
return { Value: lowerKipValue(expr) };
|
|
578
1188
|
}
|
|
579
1189
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1190
|
+
/** True when nothing in the subtree needs binding at execution time. */
|
|
1191
|
+
function isFullyLiteral(expr) {
|
|
1192
|
+
switch (expr.kind) {
|
|
1193
|
+
case 'StringLiteral':
|
|
1194
|
+
case 'NumberLiteral':
|
|
1195
|
+
case 'BooleanLiteral':
|
|
1196
|
+
case 'NullLiteral':
|
|
1197
|
+
return true;
|
|
1198
|
+
case 'ArrayLiteral':
|
|
1199
|
+
return expr.elements.every(isFullyLiteral);
|
|
1200
|
+
case 'ObjectLiteral':
|
|
1201
|
+
return expr.entries.every((e) => isFullyLiteral(e.value));
|
|
1202
|
+
case 'UnaryExpression':
|
|
1203
|
+
return expr.operator === '-' && isFullyLiteral(expr.operand);
|
|
1204
|
+
default:
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
586
1207
|
}
|
|
587
|
-
function
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
};
|
|
599
|
-
case 'METADATA':
|
|
1208
|
+
function lowerUpdateExpr(expr, targetVar) {
|
|
1209
|
+
switch (expr.kind) {
|
|
1210
|
+
case 'FunctionCallExpr': {
|
|
1211
|
+
const func = UPDATE_FUNCTIONS.get(expr.name.toUpperCase());
|
|
1212
|
+
if (!func) {
|
|
1213
|
+
throw invalidSyntax(`${expr.name} is not a KIP update function; expected ADD, MUL, CLAMP or COALESCE`, expr.range);
|
|
1214
|
+
}
|
|
1215
|
+
const arity = UPDATE_ARITY[func];
|
|
1216
|
+
if (expr.args.length !== arity) {
|
|
1217
|
+
throw invalidSyntax(`${expr.name} takes ${arity} arguments, found ${expr.args.length}`, expr.range);
|
|
1218
|
+
}
|
|
600
1219
|
return {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
where_clauses
|
|
1220
|
+
Function: {
|
|
1221
|
+
func,
|
|
1222
|
+
args: expr.args.map((arg) => lowerUpdateExpr(arg, targetVar))
|
|
605
1223
|
}
|
|
606
1224
|
};
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1225
|
+
}
|
|
1226
|
+
case 'ParameterRef':
|
|
1227
|
+
return { Param: paramName(expr.name) };
|
|
1228
|
+
case 'NumberLiteral':
|
|
1229
|
+
return { Number: expr.value };
|
|
1230
|
+
case 'UnaryExpression':
|
|
1231
|
+
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1232
|
+
return { Number: -expr.operand.value };
|
|
613
1233
|
}
|
|
614
|
-
|
|
1234
|
+
throw invalidSyntax(`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`, expr.range);
|
|
1235
|
+
case 'VariableRef':
|
|
1236
|
+
case 'FieldAccess': {
|
|
1237
|
+
const path = lowerDotPath(expr);
|
|
1238
|
+
guardOwnField(path, targetVar, expr.range);
|
|
1239
|
+
return { Variable: path };
|
|
1240
|
+
}
|
|
1241
|
+
default:
|
|
1242
|
+
throw invalidSyntax(`expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`, expr.range);
|
|
615
1243
|
}
|
|
616
1244
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* An update expression may read only the element being updated.
|
|
1247
|
+
*
|
|
1248
|
+
* Reading another variable would make the result depend on a join the
|
|
1249
|
+
* statement never declared, so each matched element must be computable from
|
|
1250
|
+
* its own row.
|
|
1251
|
+
*/
|
|
1252
|
+
function guardOwnField(path, targetVar, range) {
|
|
1253
|
+
if (targetVar !== null && path.var !== targetVar) {
|
|
1254
|
+
throw invalidSyntax(`an update expression may read only the target ?${targetVar}, found ?${path.var}`, range);
|
|
620
1255
|
}
|
|
621
|
-
return keys;
|
|
622
1256
|
}
|
|
623
1257
|
// ---------------------------------------------------------------------------
|
|
624
1258
|
// META
|
|
625
1259
|
// ---------------------------------------------------------------------------
|
|
626
|
-
function
|
|
627
|
-
switch (stmt.
|
|
628
|
-
case '
|
|
629
|
-
return
|
|
630
|
-
case '
|
|
631
|
-
return
|
|
632
|
-
case '
|
|
1260
|
+
function lowerMeta(stmt) {
|
|
1261
|
+
switch (stmt.kind) {
|
|
1262
|
+
case 'DescribeStatement':
|
|
1263
|
+
return { Describe: lowerDescribe(stmt) };
|
|
1264
|
+
case 'ListStatement':
|
|
1265
|
+
return { List: lowerList(stmt) };
|
|
1266
|
+
case 'SearchStatement':
|
|
1267
|
+
return { Search: lowerSearch(stmt) };
|
|
1268
|
+
case 'VerifyStatement':
|
|
633
1269
|
return {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1270
|
+
Verify: {
|
|
1271
|
+
target: VERIFY_TARGETS[stmt.target],
|
|
1272
|
+
value: lowerScalar(stmt.value)
|
|
637
1273
|
}
|
|
638
1274
|
};
|
|
639
|
-
case '
|
|
1275
|
+
case 'ValidateStatement':
|
|
640
1276
|
return {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1277
|
+
Validate: {
|
|
1278
|
+
target: VALIDATE_TARGETS[stmt.target],
|
|
1279
|
+
value: lowerScalar(stmt.value),
|
|
1280
|
+
options: stmt.options ? lowerBoundObject(stmt.options) : null
|
|
644
1281
|
}
|
|
645
1282
|
};
|
|
646
|
-
case '
|
|
647
|
-
return {
|
|
648
|
-
|
|
649
|
-
|
|
1283
|
+
case 'PreviewStatement':
|
|
1284
|
+
return {
|
|
1285
|
+
Preview: stmt.target === 'KML'
|
|
1286
|
+
? { Kml: lowerScalar(stmt.value) }
|
|
1287
|
+
: {
|
|
1288
|
+
ImportCapsule: {
|
|
1289
|
+
capsule: lowerScalar(stmt.value),
|
|
1290
|
+
into: lowerScalar(stmt.into)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
case 'HistoryStatement':
|
|
1295
|
+
return { History: lowerHistory(stmt) };
|
|
1296
|
+
case 'ChangesStatement':
|
|
1297
|
+
return {
|
|
1298
|
+
Changes: stmt.mode === 'SINCE'
|
|
1299
|
+
? {
|
|
1300
|
+
Since: {
|
|
1301
|
+
cursor: lowerScalar(stmt.value),
|
|
1302
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
: {
|
|
1306
|
+
AfterSeq: {
|
|
1307
|
+
seq: lowerScalar(stmt.value),
|
|
1308
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
case 'SnapshotStatement':
|
|
1313
|
+
return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } };
|
|
1314
|
+
case 'ExportCapsuleStatement':
|
|
1315
|
+
return { ExportCapsule: lowerExport(stmt) };
|
|
1316
|
+
default:
|
|
1317
|
+
throw invalidSyntax(`${stmt.kind} is not an executable KIP command`, stmt.range);
|
|
650
1318
|
}
|
|
651
1319
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1320
|
+
const VERIFY_TARGETS = {
|
|
1321
|
+
CAPSULE: 'Capsule',
|
|
1322
|
+
SCHEMA_PACKAGE: 'SchemaPackage',
|
|
1323
|
+
RECEIPT: 'Receipt',
|
|
1324
|
+
BLOB: 'Blob',
|
|
1325
|
+
CHECKPOINT: 'Checkpoint'
|
|
1326
|
+
};
|
|
1327
|
+
const VALIDATE_TARGETS = {
|
|
1328
|
+
KQL: 'Kql',
|
|
1329
|
+
KML: 'Kml',
|
|
1330
|
+
CAPSULE: 'Capsule',
|
|
1331
|
+
SCHEMA_PACKAGE: 'SchemaPackage',
|
|
1332
|
+
IMPORT_PLAN: 'ImportPlan'
|
|
1333
|
+
};
|
|
1334
|
+
const LIST_TARGETS = {
|
|
1335
|
+
SPACES: 'Spaces',
|
|
1336
|
+
SCHEMA_PACKAGES: 'SchemaPackages',
|
|
1337
|
+
TYPES: 'Types',
|
|
1338
|
+
PREDICATES: 'Predicates',
|
|
1339
|
+
FACETS: 'Facets',
|
|
1340
|
+
STRUCTURAL_FIELDS: 'StructuralFields',
|
|
1341
|
+
EPISTEMIC_POLICIES: 'EpistemicPolicies'
|
|
1342
|
+
};
|
|
1343
|
+
const SEARCH_TARGETS = {
|
|
1344
|
+
CONCEPT: 'Concept',
|
|
1345
|
+
PROPOSITION: 'Proposition',
|
|
1346
|
+
ASSERTION: 'Assertion',
|
|
1347
|
+
EVIDENCE: 'Evidence',
|
|
1348
|
+
ACTIVITY: 'Activity',
|
|
1349
|
+
COGNITION: 'Cognition'
|
|
1350
|
+
};
|
|
1351
|
+
function lowerDescribe(stmt) {
|
|
1352
|
+
const value = () => {
|
|
1353
|
+
if (!stmt.value) {
|
|
1354
|
+
throw invalidSyntax(`DESCRIBE ${stmt.target} requires an operand`, stmt.range);
|
|
1355
|
+
}
|
|
1356
|
+
return lowerScalar(stmt.value);
|
|
1357
|
+
};
|
|
1358
|
+
switch (stmt.target) {
|
|
1359
|
+
case 'PRIMER':
|
|
1360
|
+
return { Primer: { mode: stmt.mode ? lowerScalar(stmt.mode) : null } };
|
|
1361
|
+
case 'PROTOCOL':
|
|
1362
|
+
return 'Protocol';
|
|
1363
|
+
case 'EXECUTION_CONTEXT':
|
|
1364
|
+
return 'ExecutionContext';
|
|
1365
|
+
case 'CAPABILITIES':
|
|
1366
|
+
return 'Capabilities';
|
|
1367
|
+
case 'SPACE':
|
|
1368
|
+
return { Space: { value: stmt.value ? lowerScalar(stmt.value) : null } };
|
|
1369
|
+
case 'SCHEMA_ENVIRONMENT':
|
|
1370
|
+
return {
|
|
1371
|
+
SchemaEnvironment: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null }
|
|
1372
|
+
};
|
|
1373
|
+
case 'PACKAGE':
|
|
1374
|
+
return { Package: value() };
|
|
1375
|
+
case 'TYPE':
|
|
1376
|
+
return { Type: value() };
|
|
1377
|
+
case 'PREDICATE':
|
|
1378
|
+
return { Predicate: value() };
|
|
1379
|
+
case 'FACET':
|
|
1380
|
+
return { Facet: value() };
|
|
1381
|
+
case 'STRUCTURAL_FIELD':
|
|
1382
|
+
return { StructuralField: value() };
|
|
1383
|
+
case 'COMPATIBILITY':
|
|
1384
|
+
if (!stmt.from || !stmt.to) {
|
|
1385
|
+
throw invalidSyntax('DESCRIBE COMPATIBILITY requires FROM and TO', stmt.range);
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
Compatibility: { from: lowerScalar(stmt.from), to: lowerScalar(stmt.to) }
|
|
1389
|
+
};
|
|
1390
|
+
case 'ERROR':
|
|
1391
|
+
return { Error: value() };
|
|
1392
|
+
case 'TRANSACTION':
|
|
1393
|
+
return { Transaction: value() };
|
|
1394
|
+
case 'TRANSACTION_BY_IDEMPOTENCY_KEY':
|
|
1395
|
+
return { TransactionByIdempotencyKey: value() };
|
|
1396
|
+
case 'SNAPSHOT':
|
|
1397
|
+
return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } };
|
|
1398
|
+
case 'CAPSULE':
|
|
1399
|
+
return { Capsule: value() };
|
|
1400
|
+
case 'EPISTEMIC_POLICY':
|
|
1401
|
+
return {
|
|
1402
|
+
EpistemicPolicy: { value: stmt.value ? lowerScalar(stmt.value) : null }
|
|
1403
|
+
};
|
|
1404
|
+
case 'PROJECTION_CAPABILITY':
|
|
1405
|
+
return 'ProjectionCapability';
|
|
1406
|
+
case 'TRUST':
|
|
1407
|
+
return { Trust: { value: stmt.value ? lowerScalar(stmt.value) : null } };
|
|
1408
|
+
case 'ACCESS':
|
|
1409
|
+
return {
|
|
1410
|
+
Access: { with: stmt.with ? lowerBoundObject(stmt.with) : null }
|
|
1411
|
+
};
|
|
659
1412
|
}
|
|
660
|
-
return stmt.typeName;
|
|
661
1413
|
}
|
|
662
|
-
function
|
|
663
|
-
|
|
664
|
-
target: stmt.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
: requireLiteralString(stmt.withTypeValue, stmt.withType, 'WITH TYPE', stmt.range),
|
|
669
|
-
limit: lowerLimit(stmt.limit)
|
|
1414
|
+
function lowerList(stmt) {
|
|
1415
|
+
return {
|
|
1416
|
+
target: LIST_TARGETS[stmt.target],
|
|
1417
|
+
status: stmt.status ? lowerScalar(stmt.status) : null,
|
|
1418
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1419
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
670
1420
|
};
|
|
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
1421
|
}
|
|
692
|
-
function
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1422
|
+
function lowerSearch(stmt) {
|
|
1423
|
+
return {
|
|
1424
|
+
target: SEARCH_TARGETS[stmt.searchKind],
|
|
1425
|
+
term: lowerScalar(stmt.term),
|
|
1426
|
+
with_type: stmt.withType ? lowerScalar(stmt.withType) : null,
|
|
1427
|
+
with_predicate: stmt.withPredicate ? lowerScalar(stmt.withPredicate) : null,
|
|
1428
|
+
mode: stmt.mode ? lowerScalar(stmt.mode) : null,
|
|
1429
|
+
threshold: stmt.threshold ? lowerScalar(stmt.threshold) : null,
|
|
1430
|
+
as_of_seq: stmt.asOfSeq ? lowerScalar(stmt.asOfSeq) : null,
|
|
1431
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1432
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
1433
|
+
};
|
|
703
1434
|
}
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
|
|
1435
|
+
function lowerHistory(stmt) {
|
|
1436
|
+
const paging = {
|
|
1437
|
+
from_seq: stmt.fromSeq ? lowerScalar(stmt.fromSeq) : null,
|
|
1438
|
+
to_seq: stmt.toSeq ? lowerScalar(stmt.toSeq) : null,
|
|
1439
|
+
limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
|
|
1440
|
+
cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
|
|
1441
|
+
};
|
|
1442
|
+
if (stmt.target === 'SPACE') {
|
|
1443
|
+
return { Space: paging };
|
|
707
1444
|
}
|
|
708
|
-
if (!
|
|
709
|
-
throw invalidSyntax(
|
|
1445
|
+
if (!stmt.value) {
|
|
1446
|
+
throw invalidSyntax('HISTORY ELEMENT requires an element id', stmt.range);
|
|
710
1447
|
}
|
|
711
|
-
return value;
|
|
1448
|
+
return { Element: { value: lowerScalar(stmt.value), ...paging } };
|
|
1449
|
+
}
|
|
1450
|
+
function lowerExport(stmt) {
|
|
1451
|
+
return {
|
|
1452
|
+
target: lowerElementRef(stmt.target),
|
|
1453
|
+
where_clauses: lowerWhere(stmt.where),
|
|
1454
|
+
options: stmt.options ? lowerBoundObject(stmt.options) : null,
|
|
1455
|
+
as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null
|
|
1456
|
+
};
|
|
712
1457
|
}
|
|
713
1458
|
// ---------------------------------------------------------------------------
|
|
714
|
-
//
|
|
1459
|
+
// Leaf conversions
|
|
715
1460
|
// ---------------------------------------------------------------------------
|
|
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);
|
|
722
|
-
}
|
|
723
|
-
const n = BigInt(value.raw);
|
|
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);
|
|
1461
|
+
function lowerScalar(value) {
|
|
1462
|
+
if (value.kind === 'ParameterRef') {
|
|
1463
|
+
return { Param: paramName(value.name) };
|
|
729
1464
|
}
|
|
730
|
-
return
|
|
1465
|
+
return { Literal: lowerKipValue(value) };
|
|
731
1466
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (value.kind !== 'StringLiteral') {
|
|
737
|
-
throw invalidSyntax('CURSOR takes a quoted pagination token', value.range);
|
|
1467
|
+
/** An ASSERT member used where the grammar needs a scalar, e.g. `key:`. */
|
|
1468
|
+
function lowerScalarExpression(expr) {
|
|
1469
|
+
if (expr.kind === 'ParameterRef') {
|
|
1470
|
+
return { Param: paramName(expr.name) };
|
|
738
1471
|
}
|
|
739
|
-
if (
|
|
740
|
-
|
|
1472
|
+
if (expr.kind === 'StringLiteral' ||
|
|
1473
|
+
expr.kind === 'NumberLiteral' ||
|
|
1474
|
+
expr.kind === 'BooleanLiteral' ||
|
|
1475
|
+
expr.kind === 'NullLiteral') {
|
|
1476
|
+
return { Literal: lowerKipValue(expr) };
|
|
741
1477
|
}
|
|
742
|
-
|
|
1478
|
+
throw invalidSyntax(`expected a literal or :parameter, found ${describeExpression(expr)}`, expr.range);
|
|
743
1479
|
}
|
|
744
|
-
function
|
|
745
|
-
return
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const out = {};
|
|
749
|
-
for (const entry of entries) {
|
|
750
|
-
if (Object.prototype.hasOwnProperty.call(out, entry.key)) {
|
|
751
|
-
throw invalidSyntax(`duplicate key in object (keys must be unique): ${entry.key}`, entry.range);
|
|
752
|
-
}
|
|
753
|
-
out[entry.key] = lowerJson(entry.value);
|
|
754
|
-
}
|
|
755
|
-
return out;
|
|
1480
|
+
function lowerSymbol(symbol) {
|
|
1481
|
+
return symbol.kind === 'ParameterRef'
|
|
1482
|
+
? { Param: paramName(symbol.name) }
|
|
1483
|
+
: { Name: symbol.parsed };
|
|
756
1484
|
}
|
|
757
|
-
function
|
|
758
|
-
switch (
|
|
1485
|
+
function lowerElementRef(ref) {
|
|
1486
|
+
switch (ref.kind) {
|
|
1487
|
+
case 'VariableRef':
|
|
1488
|
+
return { Handle: varName(ref.name, ref.range) };
|
|
1489
|
+
case 'ParameterRef':
|
|
1490
|
+
return { Param: paramName(ref.name) };
|
|
759
1491
|
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);
|
|
1492
|
+
return { Id: ref.parsed };
|
|
773
1493
|
}
|
|
774
1494
|
}
|
|
775
|
-
/**
|
|
776
|
-
* Reads a literal in matcher or FILTER position.
|
|
777
|
-
*
|
|
778
|
-
* Stricter than {@link lowerJson}: these positions are not JSON-value
|
|
779
|
-
* positions in the grammar, and a trailing comma that an attribute block
|
|
780
|
-
* tolerates is a syntax error inside `IN [...]`.
|
|
781
|
-
*/
|
|
782
1495
|
function lowerKipValue(expr) {
|
|
783
|
-
if ((expr.kind === 'ArrayLiteral' || expr.kind === 'ObjectLiteral') &&
|
|
784
|
-
expr.trailingComma) {
|
|
785
|
-
throw invalidSyntax('a literal list takes no trailing comma', expr.range);
|
|
786
|
-
}
|
|
787
1496
|
switch (expr.kind) {
|
|
788
1497
|
case 'StringLiteral':
|
|
789
1498
|
return { String: expr.parsed };
|
|
790
1499
|
case 'NumberLiteral':
|
|
791
|
-
|
|
1500
|
+
if (!Number.isFinite(expr.value)) {
|
|
1501
|
+
throw invalidSyntax(`only finite numbers are valid KIP literals, found ${expr.raw}`, expr.range);
|
|
1502
|
+
}
|
|
1503
|
+
return { Number: expr.value };
|
|
792
1504
|
case 'BooleanLiteral':
|
|
793
1505
|
return { Bool: expr.value };
|
|
794
1506
|
case 'NullLiteral':
|
|
795
1507
|
return 'Null';
|
|
796
1508
|
case 'ArrayLiteral':
|
|
797
1509
|
return { Array: expr.elements.map(lowerKipValue) };
|
|
798
|
-
case 'ObjectLiteral':
|
|
1510
|
+
case 'ObjectLiteral':
|
|
1511
|
+
case 'ObjectPattern': {
|
|
1512
|
+
const entries = expr.kind === 'ObjectLiteral' ? expr.entries : expr.members;
|
|
799
1513
|
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
|
-
}
|
|
1514
|
+
for (const entry of entries) {
|
|
804
1515
|
out[entry.key] = lowerKipValue(entry.value);
|
|
805
1516
|
}
|
|
806
1517
|
return { Object: out };
|
|
807
1518
|
}
|
|
1519
|
+
case 'UnaryExpression':
|
|
1520
|
+
if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
|
|
1521
|
+
return { Number: -expr.operand.value };
|
|
1522
|
+
}
|
|
1523
|
+
throw invalidSyntax(`expected a value, found ${describeExpression(expr)}`, expr.range);
|
|
808
1524
|
default:
|
|
809
|
-
throw invalidSyntax(`expected a
|
|
1525
|
+
throw invalidSyntax(`expected a value, found ${describeExpression(expr)}`, expr.range);
|
|
810
1526
|
}
|
|
811
1527
|
}
|
|
812
1528
|
/**
|
|
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.
|
|
1529
|
+
* Option and epistemic blocks are `data_value`s, not plain JSON: the grammar
|
|
1530
|
+
* lets a parameter stand anywhere inside them.
|
|
821
1531
|
*/
|
|
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);
|
|
1532
|
+
function lowerBoundObject(object) {
|
|
1533
|
+
const out = {};
|
|
1534
|
+
for (const entry of object.entries) {
|
|
1535
|
+
out[entry.key] = lowerBoundValue(entry.value, null);
|
|
855
1536
|
}
|
|
856
|
-
return
|
|
1537
|
+
return out;
|
|
857
1538
|
}
|
|
858
1539
|
/** Strips the `?` sigil; the executable form carries bare names. */
|
|
859
1540
|
function varName(name, range) {
|
|
@@ -862,14 +1543,22 @@ function varName(name, range) {
|
|
|
862
1543
|
}
|
|
863
1544
|
return name.slice(1);
|
|
864
1545
|
}
|
|
1546
|
+
/** Strips the `:` sigil; the executable form carries bare names. */
|
|
1547
|
+
function paramName(name) {
|
|
1548
|
+
return name.startsWith(':') ? name.slice(1) : name;
|
|
1549
|
+
}
|
|
865
1550
|
function describeExpression(expr) {
|
|
866
1551
|
switch (expr.kind) {
|
|
867
1552
|
case 'ParameterRef':
|
|
868
|
-
return `the parameter ${expr.name}
|
|
1553
|
+
return `the parameter ${expr.name}`;
|
|
869
1554
|
case 'VariableRef':
|
|
870
1555
|
return `the variable ${expr.name}`;
|
|
871
1556
|
case 'FunctionCallExpr':
|
|
872
1557
|
return `a call to ${expr.name}`;
|
|
1558
|
+
case 'AggregateExpr':
|
|
1559
|
+
return `the aggregate ${expr.name}`;
|
|
1560
|
+
case 'BinaryExpression':
|
|
1561
|
+
return `the operator ${expr.operator}`;
|
|
873
1562
|
default:
|
|
874
1563
|
return expr.kind;
|
|
875
1564
|
}
|