@ldclabs/kip-lang 0.4.0 → 2.0.1

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