@ldclabs/kip-lang 0.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +133 -44
- package/dist/ast.d.ts +519 -144
- package/dist/ast.d.ts.map +1 -1
- package/dist/budget.d.ts +37 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +105 -0
- package/dist/budget.js.map +1 -0
- package/dist/diagnostics.d.ts +8 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +32 -3
- package/dist/diagnostics.js.map +1 -1
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/exec-ast.d.ts +679 -0
- package/dist/exec-ast.d.ts.map +1 -0
- package/dist/exec-ast.js +24 -0
- package/dist/exec-ast.js.map +1 -0
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +870 -479
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +46 -33
- package/dist/lexer.js.map +1 -1
- package/dist/lower.d.ts +17 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +1566 -0
- package/dist/lower.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +2476 -1152
- package/dist/parser.js.map +1 -1
- package/dist/semantics.d.ts +11 -7
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +295 -180
- package/dist/semantics.js.map +1 -1
- package/dist/token.d.ts +130 -40
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +264 -83
- package/dist/token.js.map +1 -1
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +36 -6
- package/src/ast.ts +914 -0
- package/src/budget.ts +108 -0
- package/src/diagnostics.ts +182 -0
- package/src/errors.ts +42 -0
- package/src/exec-ast.ts +614 -0
- package/src/formatter.ts +1339 -0
- package/src/index.ts +226 -0
- package/src/lexer.ts +459 -0
- package/src/lower.ts +2011 -0
- package/src/parser.ts +3506 -0
- package/src/semantics.ts +392 -0
- package/src/token.ts +408 -0
- package/src/version.ts +13 -0
package/dist/lower.js
ADDED
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
import { invalidSyntax } from './errors.js';
|
|
2
|
+
const AGGREGATIONS = new Map([
|
|
3
|
+
['COUNT', 'Count'],
|
|
4
|
+
['SUM', 'Sum'],
|
|
5
|
+
['AVG', 'Avg'],
|
|
6
|
+
['MIN', 'Min'],
|
|
7
|
+
['MAX', 'Max']
|
|
8
|
+
]);
|
|
9
|
+
const FILTER_FUNCTIONS = new Map([
|
|
10
|
+
['CONTAINS', 'Contains'],
|
|
11
|
+
['STARTS_WITH', 'StartsWith'],
|
|
12
|
+
['ENDS_WITH', 'EndsWith'],
|
|
13
|
+
['REGEX', 'Regex'],
|
|
14
|
+
['IN', 'In'],
|
|
15
|
+
['IS_NULL', 'IsNull'],
|
|
16
|
+
['IS_NOT_NULL', 'IsNotNull'],
|
|
17
|
+
['IS_LITERAL', 'IsLiteral'],
|
|
18
|
+
['IS_ELEMENT', 'IsElement'],
|
|
19
|
+
['IS_KIND', 'IsKind'],
|
|
20
|
+
['LITERAL_TYPE', 'LiteralType']
|
|
21
|
+
]);
|
|
22
|
+
const UPDATE_FUNCTIONS = new Map([
|
|
23
|
+
['ADD', 'Add'],
|
|
24
|
+
['MUL', 'Mul'],
|
|
25
|
+
['CLAMP', 'Clamp'],
|
|
26
|
+
['COALESCE', 'Coalesce']
|
|
27
|
+
]);
|
|
28
|
+
const UPDATE_ARITY = {
|
|
29
|
+
Add: 2,
|
|
30
|
+
Mul: 2,
|
|
31
|
+
Coalesce: 2,
|
|
32
|
+
Clamp: 3
|
|
33
|
+
};
|
|
34
|
+
const COMPARISONS = new Map([
|
|
35
|
+
['==', 'Equal'],
|
|
36
|
+
['!=', 'NotEqual'],
|
|
37
|
+
['<', 'LessThan'],
|
|
38
|
+
['>', 'GreaterThan'],
|
|
39
|
+
['<=', 'LessEqual'],
|
|
40
|
+
['>=', 'GreaterEqual']
|
|
41
|
+
]);
|
|
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']);
|
|
83
|
+
/**
|
|
84
|
+
* Lowers a parsed program carrying exactly one command.
|
|
85
|
+
*
|
|
86
|
+
* KIP's request envelope binds one command to one result, so a source text
|
|
87
|
+
* that holds two statements is not a command — it is a batch, and silently
|
|
88
|
+
* running the first would answer a question the caller did not ask. Use
|
|
89
|
+
* {@link lowerAll} for multi-statement text such as a schema capsule.
|
|
90
|
+
*
|
|
91
|
+
* @throws {KipSyntaxError} on anything that is not one executable command.
|
|
92
|
+
*/
|
|
93
|
+
export function lower(program) {
|
|
94
|
+
const [first, second] = program.statements;
|
|
95
|
+
if (!first) {
|
|
96
|
+
throw invalidSyntax('expected a KIP command, found none');
|
|
97
|
+
}
|
|
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);
|
|
101
|
+
}
|
|
102
|
+
return lowerStatement(first);
|
|
103
|
+
}
|
|
104
|
+
/** Lowers every statement in a multi-command source text. */
|
|
105
|
+
export function lowerAll(program) {
|
|
106
|
+
if (program.statements.length === 0) {
|
|
107
|
+
throw invalidSyntax('expected at least one KIP command, found none');
|
|
108
|
+
}
|
|
109
|
+
return program.statements.map(lowerStatement);
|
|
110
|
+
}
|
|
111
|
+
export function lowerStatement(stmt) {
|
|
112
|
+
switch (stmt.kind) {
|
|
113
|
+
case 'FindStatement':
|
|
114
|
+
return { Kql: lowerFind(stmt) };
|
|
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':
|
|
124
|
+
case 'UpdateStatement':
|
|
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
|
+
}
|
|
142
|
+
};
|
|
143
|
+
default:
|
|
144
|
+
return { Meta: lowerMeta(stmt) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// KQL
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
function lowerFind(stmt) {
|
|
151
|
+
if (stmt.projections.length === 0) {
|
|
152
|
+
throw invalidSyntax('FIND requires at least one projection', stmt.range);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
find_clause: {
|
|
156
|
+
expressions: stmt.projections.map((p) => lowerFindExpression(p))
|
|
157
|
+
},
|
|
158
|
+
where_clauses: lowerWhere(stmt.where),
|
|
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
|
|
165
|
+
};
|
|
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
|
+
}
|
|
178
|
+
function lowerFindExpression(expr) {
|
|
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);
|
|
183
|
+
}
|
|
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
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
variable: lowerDotPath(item.expression),
|
|
209
|
+
direction,
|
|
210
|
+
aggregation: null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
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: [] };
|
|
217
|
+
}
|
|
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);
|
|
228
|
+
}
|
|
229
|
+
function lowerWherePattern(pattern) {
|
|
230
|
+
switch (pattern.kind) {
|
|
231
|
+
case 'ConceptPattern':
|
|
232
|
+
return {
|
|
233
|
+
Concept: {
|
|
234
|
+
variable: varName(pattern.variable.name, pattern.variable.range),
|
|
235
|
+
matcher: lowerObjectMatcher(pattern.matcher)
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
case 'PropositionPattern':
|
|
239
|
+
return {
|
|
240
|
+
Proposition: {
|
|
241
|
+
variable: pattern.variable
|
|
242
|
+
? varName(pattern.variable.name, pattern.variable.range)
|
|
243
|
+
: null,
|
|
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)
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
case 'FilterClause':
|
|
318
|
+
return { Filter: { expression: lowerFilter(pattern.expression) } };
|
|
319
|
+
case 'NotClause':
|
|
320
|
+
return { Not: pattern.patterns.map(lowerWherePattern) };
|
|
321
|
+
case 'OptionalClause':
|
|
322
|
+
return { Optional: pattern.patterns.map(lowerWherePattern) };
|
|
323
|
+
case 'UnionClause':
|
|
324
|
+
return { Union: pattern.patterns.map(lowerWherePattern) };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
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);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
Tuple: {
|
|
335
|
+
subject: lowerPropositionSubject(tuple.subject),
|
|
336
|
+
predicate: lowerPredicate(tuple.predicate),
|
|
337
|
+
object: lowerTerm(tuple.object)
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Resolves the tuple a resolve-or-create statement needs.
|
|
343
|
+
*
|
|
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.
|
|
347
|
+
*/
|
|
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);
|
|
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);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
subject: lowerPropositionSubject(tuple.subject),
|
|
363
|
+
predicate: predicate.Atom,
|
|
364
|
+
object: lowerTerm(tuple.object)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
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) {
|
|
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);
|
|
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) };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Filters
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
function lowerFilter(expr) {
|
|
446
|
+
switch (expr.kind) {
|
|
447
|
+
case 'BinaryExpression': {
|
|
448
|
+
if (expr.operator === '&&' || expr.operator === '||') {
|
|
449
|
+
return {
|
|
450
|
+
Logical: {
|
|
451
|
+
left: lowerFilter(expr.left),
|
|
452
|
+
operator: expr.operator === '&&' ? 'And' : 'Or',
|
|
453
|
+
right: lowerFilter(expr.right)
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const operator = COMPARISONS.get(expr.operator);
|
|
458
|
+
if (!operator) {
|
|
459
|
+
throw invalidSyntax(`unknown comparison operator ${expr.operator}`, expr.range);
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
Comparison: {
|
|
463
|
+
left: lowerFilterOperand(expr.left),
|
|
464
|
+
operator,
|
|
465
|
+
right: lowerFilterOperand(expr.right)
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
case 'UnaryExpression':
|
|
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);
|
|
474
|
+
case 'FunctionCallExpr': {
|
|
475
|
+
const func = FILTER_FUNCTIONS.get(expr.name.toUpperCase());
|
|
476
|
+
if (!func) {
|
|
477
|
+
throw invalidSyntax(`${expr.name} is not a KIP filter function`, expr.range);
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
Function: { func, args: expr.args.map(lowerFilterOperand) }
|
|
481
|
+
};
|
|
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);
|
|
487
|
+
default:
|
|
488
|
+
throw invalidSyntax(`a filter must be a comparison, a logical combination, a negation or a function call, found ${describeExpression(expr)}`, expr.range);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
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);
|
|
501
|
+
}
|
|
502
|
+
return { List: expr.elements.map(lowerFilterOperand) };
|
|
503
|
+
case 'UnaryExpression':
|
|
504
|
+
if (expr.operator === '-') {
|
|
505
|
+
return { Negate: lowerFilterOperand(expr.operand) };
|
|
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) };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
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);
|
|
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)
|
|
726
|
+
: null,
|
|
727
|
+
set_attributes: stmt.setAttributes
|
|
728
|
+
? lowerAssignments(stmt.setAttributes.assignments, null)
|
|
729
|
+
: null,
|
|
730
|
+
set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
|
|
731
|
+
set_structural: stmt.setStructural
|
|
732
|
+
? lowerStructural(stmt.setStructural, null)
|
|
733
|
+
: null
|
|
734
|
+
};
|
|
735
|
+
}
|
|
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)
|
|
760
|
+
: null,
|
|
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
|
|
775
|
+
};
|
|
776
|
+
}
|
|
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
|
+
}
|
|
830
|
+
}
|
|
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);
|
|
837
|
+
}
|
|
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;
|
|
918
|
+
}
|
|
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
|
+
}
|
|
935
|
+
}
|
|
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
|
+
};
|
|
943
|
+
}
|
|
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);
|
|
955
|
+
}
|
|
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
|
+
};
|
|
965
|
+
}
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
// UPDATE
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
function lowerUpdate(stmt) {
|
|
970
|
+
if (stmt.actions.length === 0) {
|
|
971
|
+
throw invalidSyntax('UPDATE requires at least one SET or UNSET action', stmt.range);
|
|
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));
|
|
977
|
+
return {
|
|
978
|
+
target,
|
|
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
|
|
985
|
+
};
|
|
986
|
+
}
|
|
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
|
+
}
|
|
1025
|
+
}
|
|
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) {
|
|
1106
|
+
const seen = new Set();
|
|
1107
|
+
const out = [];
|
|
1108
|
+
for (const entry of object.entries) {
|
|
1109
|
+
guardProtectedField(entry.key, entry.range);
|
|
1110
|
+
if (seen.has(entry.key)) {
|
|
1111
|
+
throw invalidSyntax(`duplicate assignment for ${entry.key}`, entry.range);
|
|
1112
|
+
}
|
|
1113
|
+
seen.add(entry.key);
|
|
1114
|
+
out.push([entry.key, lowerMutationValue(entry.value, targetVar)]);
|
|
1115
|
+
}
|
|
1116
|
+
return out;
|
|
1117
|
+
}
|
|
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);
|
|
1134
|
+
}
|
|
1135
|
+
return clause.removals.map((removal) => ({
|
|
1136
|
+
field: lowerSymbol(removal.field),
|
|
1137
|
+
value: lowerMutationValue(removal.value, targetVar)
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
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);
|
|
1145
|
+
}
|
|
1146
|
+
guardProtectedField(field.name, field.range);
|
|
1147
|
+
seen.add(field.name);
|
|
1148
|
+
}
|
|
1149
|
+
return [...seen];
|
|
1150
|
+
}
|
|
1151
|
+
function lowerMutationValue(expr, targetVar) {
|
|
1152
|
+
if (expr.kind === 'FunctionCallExpr') {
|
|
1153
|
+
return { Expr: lowerUpdateExpr(expr, targetVar) };
|
|
1154
|
+
}
|
|
1155
|
+
if (expr.kind === 'AggregateExpr') {
|
|
1156
|
+
throw invalidSyntax(`${expr.name} is an aggregate and cannot appear in an assignment`, expr.range);
|
|
1157
|
+
}
|
|
1158
|
+
return lowerBoundValue(expr, targetVar);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
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.
|
|
1164
|
+
*/
|
|
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 };
|
|
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) };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
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
|
+
}
|
|
1207
|
+
}
|
|
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
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
Function: {
|
|
1221
|
+
func,
|
|
1222
|
+
args: expr.args.map((arg) => lowerUpdateExpr(arg, targetVar))
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
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 };
|
|
1233
|
+
}
|
|
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);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
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);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// ---------------------------------------------------------------------------
|
|
1258
|
+
// META
|
|
1259
|
+
// ---------------------------------------------------------------------------
|
|
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':
|
|
1269
|
+
return {
|
|
1270
|
+
Verify: {
|
|
1271
|
+
target: VERIFY_TARGETS[stmt.target],
|
|
1272
|
+
value: lowerScalar(stmt.value)
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
case 'ValidateStatement':
|
|
1276
|
+
return {
|
|
1277
|
+
Validate: {
|
|
1278
|
+
target: VALIDATE_TARGETS[stmt.target],
|
|
1279
|
+
value: lowerScalar(stmt.value),
|
|
1280
|
+
options: stmt.options ? lowerBoundObject(stmt.options) : null
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
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);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
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
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
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
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
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
|
+
};
|
|
1434
|
+
}
|
|
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 };
|
|
1444
|
+
}
|
|
1445
|
+
if (!stmt.value) {
|
|
1446
|
+
throw invalidSyntax('HISTORY ELEMENT requires an element id', stmt.range);
|
|
1447
|
+
}
|
|
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
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
// ---------------------------------------------------------------------------
|
|
1459
|
+
// Leaf conversions
|
|
1460
|
+
// ---------------------------------------------------------------------------
|
|
1461
|
+
function lowerScalar(value) {
|
|
1462
|
+
if (value.kind === 'ParameterRef') {
|
|
1463
|
+
return { Param: paramName(value.name) };
|
|
1464
|
+
}
|
|
1465
|
+
return { Literal: lowerKipValue(value) };
|
|
1466
|
+
}
|
|
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) };
|
|
1471
|
+
}
|
|
1472
|
+
if (expr.kind === 'StringLiteral' ||
|
|
1473
|
+
expr.kind === 'NumberLiteral' ||
|
|
1474
|
+
expr.kind === 'BooleanLiteral' ||
|
|
1475
|
+
expr.kind === 'NullLiteral') {
|
|
1476
|
+
return { Literal: lowerKipValue(expr) };
|
|
1477
|
+
}
|
|
1478
|
+
throw invalidSyntax(`expected a literal or :parameter, found ${describeExpression(expr)}`, expr.range);
|
|
1479
|
+
}
|
|
1480
|
+
function lowerSymbol(symbol) {
|
|
1481
|
+
return symbol.kind === 'ParameterRef'
|
|
1482
|
+
? { Param: paramName(symbol.name) }
|
|
1483
|
+
: { Name: symbol.parsed };
|
|
1484
|
+
}
|
|
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) };
|
|
1491
|
+
case 'StringLiteral':
|
|
1492
|
+
return { Id: ref.parsed };
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function lowerKipValue(expr) {
|
|
1496
|
+
switch (expr.kind) {
|
|
1497
|
+
case 'StringLiteral':
|
|
1498
|
+
return { String: expr.parsed };
|
|
1499
|
+
case 'NumberLiteral':
|
|
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 };
|
|
1504
|
+
case 'BooleanLiteral':
|
|
1505
|
+
return { Bool: expr.value };
|
|
1506
|
+
case 'NullLiteral':
|
|
1507
|
+
return 'Null';
|
|
1508
|
+
case 'ArrayLiteral':
|
|
1509
|
+
return { Array: expr.elements.map(lowerKipValue) };
|
|
1510
|
+
case 'ObjectLiteral':
|
|
1511
|
+
case 'ObjectPattern': {
|
|
1512
|
+
const entries = expr.kind === 'ObjectLiteral' ? expr.entries : expr.members;
|
|
1513
|
+
const out = {};
|
|
1514
|
+
for (const entry of entries) {
|
|
1515
|
+
out[entry.key] = lowerKipValue(entry.value);
|
|
1516
|
+
}
|
|
1517
|
+
return { Object: out };
|
|
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);
|
|
1524
|
+
default:
|
|
1525
|
+
throw invalidSyntax(`expected a value, found ${describeExpression(expr)}`, expr.range);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Option and epistemic blocks are `data_value`s, not plain JSON: the grammar
|
|
1530
|
+
* lets a parameter stand anywhere inside them.
|
|
1531
|
+
*/
|
|
1532
|
+
function lowerBoundObject(object) {
|
|
1533
|
+
const out = {};
|
|
1534
|
+
for (const entry of object.entries) {
|
|
1535
|
+
out[entry.key] = lowerBoundValue(entry.value, null);
|
|
1536
|
+
}
|
|
1537
|
+
return out;
|
|
1538
|
+
}
|
|
1539
|
+
/** Strips the `?` sigil; the executable form carries bare names. */
|
|
1540
|
+
function varName(name, range) {
|
|
1541
|
+
if (!name.startsWith('?')) {
|
|
1542
|
+
throw invalidSyntax(`expected a variable, found ${name}`, range);
|
|
1543
|
+
}
|
|
1544
|
+
return name.slice(1);
|
|
1545
|
+
}
|
|
1546
|
+
/** Strips the `:` sigil; the executable form carries bare names. */
|
|
1547
|
+
function paramName(name) {
|
|
1548
|
+
return name.startsWith(':') ? name.slice(1) : name;
|
|
1549
|
+
}
|
|
1550
|
+
function describeExpression(expr) {
|
|
1551
|
+
switch (expr.kind) {
|
|
1552
|
+
case 'ParameterRef':
|
|
1553
|
+
return `the parameter ${expr.name}`;
|
|
1554
|
+
case 'VariableRef':
|
|
1555
|
+
return `the variable ${expr.name}`;
|
|
1556
|
+
case 'FunctionCallExpr':
|
|
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}`;
|
|
1562
|
+
default:
|
|
1563
|
+
return expr.kind;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
//# sourceMappingURL=lower.js.map
|