@ldclabs/kip-lang 0.3.1 → 0.4.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/dist/lower.js ADDED
@@ -0,0 +1,877 @@
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
+ const AGGREGATIONS = new Map([
11
+ ['COUNT', 'Count'],
12
+ ['SUM', 'Sum'],
13
+ ['AVG', 'Avg'],
14
+ ['MIN', 'Min'],
15
+ ['MAX', 'Max']
16
+ ]);
17
+ const FILTER_FUNCTIONS = new Map([
18
+ ['CONTAINS', 'Contains'],
19
+ ['STARTS_WITH', 'StartsWith'],
20
+ ['ENDS_WITH', 'EndsWith'],
21
+ ['REGEX', 'Regex'],
22
+ ['IN', 'In'],
23
+ ['IS_NULL', 'IsNull'],
24
+ ['IS_NOT_NULL', 'IsNotNull']
25
+ ]);
26
+ const UPDATE_FUNCTIONS = new Map([
27
+ ['ADD', 'Add'],
28
+ ['MUL', 'Mul'],
29
+ ['CLAMP', 'Clamp'],
30
+ ['COALESCE', 'Coalesce']
31
+ ]);
32
+ const UPDATE_ARITY = {
33
+ Add: 2,
34
+ Mul: 2,
35
+ Coalesce: 2,
36
+ Clamp: 3
37
+ };
38
+ const COMPARISONS = new Map([
39
+ ['==', 'Equal'],
40
+ ['!=', 'NotEqual'],
41
+ ['<', 'LessThan'],
42
+ ['>', 'GreaterThan'],
43
+ ['<=', 'LessEqual'],
44
+ ['>=', 'GreaterEqual']
45
+ ]);
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';
50
+ /**
51
+ * Lowers a parsed program carrying exactly one command.
52
+ *
53
+ * KIP's request envelope binds one command to one result, so a source text
54
+ * that holds two statements is not a command — it is a batch, and silently
55
+ * running the first would answer a question the caller did not ask. Use
56
+ * {@link lowerAll} for multi-statement text such as a schema capsule.
57
+ *
58
+ * @throws {KipSyntaxError} on anything that is not one executable command.
59
+ */
60
+ export function lower(program) {
61
+ const [first, second] = program.statements;
62
+ if (!first) {
63
+ throw invalidSyntax('expected a KIP command, found none');
64
+ }
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
+ };
76
+ }
77
+ throw invalidSyntax(`expected a single KIP command, found ${program.statements.length}`, second.range);
78
+ }
79
+ /** Lowers every command in a multi-statement program, in source order. */
80
+ export function lowerAll(program) {
81
+ return program.statements.map(lowerStatement);
82
+ }
83
+ /** Lowers one statement. */
84
+ export function lowerStatement(stmt) {
85
+ switch (stmt.kind) {
86
+ case 'FindStatement':
87
+ return { Kql: lowerFind(stmt) };
88
+ case 'UpsertStatement':
89
+ return { Kml: { Upsert: [lowerUpsert(stmt)] } };
90
+ 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)
105
+ };
106
+ const cursor = lowerCursor(stmt.cursor);
107
+ if (cursor !== null)
108
+ exportCmd.cursor = cursor;
109
+ return { Meta: { Export: exportCmd } };
110
+ }
111
+ }
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // KQL
115
+ // ---------------------------------------------------------------------------
116
+ function lowerFind(stmt) {
117
+ if (!stmt.where) {
118
+ throw invalidSyntax('FIND requires a WHERE clause', stmt.range);
119
+ }
120
+ return {
121
+ find_clause: { expressions: stmt.projections.map(lowerFindExpression) },
122
+ 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)
126
+ };
127
+ }
128
+ 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);
143
+ }
144
+ return { variable: arg, direction, aggregation: func };
145
+ }
146
+ return {
147
+ variable: dotPathVar(key.expression),
148
+ direction,
149
+ aggregation: null
150
+ };
151
+ }
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);
179
+ }
180
+ return clauses;
181
+ }
182
+ function lowerWherePattern(pattern) {
183
+ 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
+ }
188
+ return {
189
+ Concept: {
190
+ variable: varName(pattern.variable, pattern.range),
191
+ matcher: lowerConceptMatcher(pattern.matcher)
192
+ }
193
+ };
194
+ }
195
+ case 'PropositionPattern':
196
+ return {
197
+ Proposition: {
198
+ variable: pattern.variable
199
+ ? varName(pattern.variable, pattern.range)
200
+ : null,
201
+ matcher: lowerPropositionMatcher(pattern)
202
+ }
203
+ };
204
+ case 'FilterClause':
205
+ return { Filter: { expression: lowerFilter(pattern.expression) } };
206
+ case 'NotClause':
207
+ return { Not: nonEmptyBlock(pattern.patterns, 'NOT', pattern.range) };
208
+ case 'OptionalClause':
209
+ return {
210
+ Optional: nonEmptyBlock(pattern.patterns, 'OPTIONAL', pattern.range)
211
+ };
212
+ case 'UnionClause':
213
+ return { Union: nonEmptyBlock(pattern.patterns, 'UNION', pattern.range) };
214
+ }
215
+ }
216
+ function nonEmptyBlock(patterns, name, range) {
217
+ if (patterns.length === 0) {
218
+ throw invalidSyntax(`${name} block must contain at least one clause`, range);
219
+ }
220
+ return patterns.map(lowerWherePattern);
221
+ }
222
+ /**
223
+ * Collapses `{id: ..., type: ..., name: ...}` to the one identified form it
224
+ * expresses.
225
+ *
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.
231
+ */
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 };
289
+ }
290
+ if (!pattern.subject || !pattern.predicate || !pattern.object) {
291
+ throw invalidSyntax('a proposition clause needs a subject, a predicate and an object', pattern.range);
292
+ }
293
+ return {
294
+ Object: {
295
+ subject: lowerEndpoint(pattern.subject),
296
+ predicate: lowerPredicate(pattern.predicate),
297
+ object: lowerEndpoint(pattern.object)
298
+ }
299
+ };
300
+ }
301
+ function lowerEndpoint(endpoint) {
302
+ switch (endpoint.kind) {
303
+ 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 } };
334
+ }
335
+ }
336
+ }
337
+ // ---------------------------------------------------------------------------
338
+ // FILTER
339
+ // ---------------------------------------------------------------------------
340
+ function lowerFilter(expr) {
341
+ switch (expr.kind) {
342
+ case 'BinaryExpression': {
343
+ if (expr.operator === '&&' || expr.operator === '||') {
344
+ return {
345
+ Logical: {
346
+ left: lowerFilter(expr.left),
347
+ operator: expr.operator === '&&' ? 'And' : 'Or',
348
+ right: lowerFilter(expr.right)
349
+ }
350
+ };
351
+ }
352
+ const operator = COMPARISONS.get(expr.operator);
353
+ if (!operator) {
354
+ throw invalidSyntax(`unsupported operator ${expr.operator} in FILTER`, expr.range);
355
+ }
356
+ return {
357
+ Comparison: {
358
+ left: lowerFilterOperand(expr.left),
359
+ operator,
360
+ right: lowerFilterOperand(expr.right)
361
+ }
362
+ };
363
+ }
364
+ case 'UnaryExpression':
365
+ return { Not: lowerFilter(expr.operand) };
366
+ case 'FunctionCallExpr': {
367
+ const func = FILTER_FUNCTIONS.get(expr.name);
368
+ 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);
371
+ }
372
+ const args = expr.args.map(lowerFilterOperand);
373
+ checkFilterArity(func, args, expr.range);
374
+ return { Function: { func, args } };
375
+ }
376
+ default:
377
+ throw invalidSyntax('FILTER takes a comparison, a logical combination, or a filter function', expr.range);
378
+ }
379
+ }
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);
396
+ }
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);
402
+ }
403
+ }
404
+ }
405
+ function lowerFilterOperand(expr) {
406
+ if (expr.kind === 'VariableRef' || expr.kind === 'DotExpression') {
407
+ return { Variable: dotPathVar(expr) };
408
+ }
409
+ if (expr.kind === 'ArrayLiteral') {
410
+ if (expr.trailingComma) {
411
+ throw invalidSyntax('a literal list takes no trailing comma', expr.range);
412
+ }
413
+ return { List: expr.elements.map(lowerKipValue) };
414
+ }
415
+ return { Literal: lowerKipValue(expr) };
416
+ }
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)
435
+ : 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
+ }))
442
+ : null,
443
+ metadata: lowerMetadata(block.metadata)
444
+ };
445
+ const version = lowerExpectVersion(block);
446
+ if (version !== undefined)
447
+ out.expect_version = version;
448
+ return out;
449
+ }
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)
458
+ : null,
459
+ metadata: lowerMetadata(block.metadata)
460
+ };
461
+ const version = lowerExpectVersion(block);
462
+ if (version !== undefined)
463
+ out.expect_version = version;
464
+ return out;
465
+ }
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);
472
+ }
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);
476
+ }
477
+ return Number(n);
478
+ }
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;
486
+ }
487
+ requireIdentityProposition(term.Proposition, range);
488
+ return term;
489
+ }
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);
495
+ }
496
+ requireIdentityTarget(matcher.Object.subject, range);
497
+ requireIdentityTarget(matcher.Object.object, range);
498
+ }
499
+ 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);
505
+ }
506
+ return {
507
+ target,
508
+ set_attributes,
509
+ set_metadata,
510
+ where_clauses: lowerWhere(stmt.where),
511
+ limit: lowerLimit(stmt.limit)
512
+ };
513
+ }
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);
519
+ }
520
+ const out = [];
521
+ const seen = new Set();
522
+ for (const entry of block.entries) {
523
+ if (seen.has(entry.key)) {
524
+ throw invalidSyntax(`duplicate key in object (keys must be unique): ${entry.key}`, entry.range);
525
+ }
526
+ seen.add(entry.key);
527
+ out.push([entry.key, lowerUpdateValue(entry.value, target, entry.key)]);
528
+ }
529
+ return out;
530
+ }
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 };
536
+ }
537
+ return { Json: lowerJson(expr) };
538
+ }
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);
548
+ }
549
+ return { Function: { func, args: expr.args.map(lowerUpdateExpr) } };
550
+ }
551
+ if (expr.kind === 'VariableRef' || expr.kind === 'DotExpression') {
552
+ return { Variable: dotPathVar(expr) };
553
+ }
554
+ if (expr.kind === 'NumberLiteral') {
555
+ return { Number: numberValue(expr.value, expr.raw, expr.range) };
556
+ }
557
+ throw invalidSyntax('an UPDATE expression operand is a number, a ?target dot-path, or a nested expression', expr.range);
558
+ }
559
+ /**
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.
565
+ */
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);
577
+ }
578
+ }
579
+ }
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
+ };
586
+ }
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':
600
+ return {
601
+ DeleteMetadata: {
602
+ keys: requireKeys(stmt.keys, 'METADATA', stmt.range),
603
+ target,
604
+ where_clauses
605
+ }
606
+ };
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);
613
+ }
614
+ return { DeleteConcept: { target, where_clauses } };
615
+ }
616
+ }
617
+ function requireKeys(keys, what, range) {
618
+ if (!keys || keys.length === 0) {
619
+ throw invalidSyntax(`DELETE ${what} needs at least one key`, range);
620
+ }
621
+ return keys;
622
+ }
623
+ // ---------------------------------------------------------------------------
624
+ // META
625
+ // ---------------------------------------------------------------------------
626
+ function lowerDescribe(stmt) {
627
+ switch (stmt.describeType) {
628
+ case 'PRIMER':
629
+ return 'Primer';
630
+ case 'DOMAINS':
631
+ return 'Domains';
632
+ case 'CONCEPT_TYPES':
633
+ return {
634
+ ConceptTypes: {
635
+ limit: lowerLimit(stmt.limit),
636
+ cursor: lowerCursor(stmt.cursor)
637
+ }
638
+ };
639
+ case 'PROPOSITION_TYPES':
640
+ return {
641
+ PropositionTypes: {
642
+ limit: lowerLimit(stmt.limit),
643
+ cursor: lowerCursor(stmt.cursor)
644
+ }
645
+ };
646
+ case 'CONCEPT_TYPE':
647
+ return { ConceptType: describeName(stmt) };
648
+ case 'PROPOSITION_TYPE':
649
+ return { PropositionType: describeName(stmt) };
650
+ }
651
+ }
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);
659
+ }
660
+ return stmt.typeName;
661
+ }
662
+ 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)
670
+ };
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
+ }
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;
702
+ }
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);
707
+ }
708
+ if (!node) {
709
+ throw invalidSyntax(`${what} must be a quoted string`, range);
710
+ }
711
+ return value;
712
+ }
713
+ // ---------------------------------------------------------------------------
714
+ // Shared leaves
715
+ // ---------------------------------------------------------------------------
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);
722
+ }
723
+ const n = BigInt(value.raw);
724
+ if (n <= 0n) {
725
+ throw invalidSyntax('LIMIT must be a positive integer (LIMIT 0 is not allowed; omit LIMIT for the engine default)', value.range);
726
+ }
727
+ if (n > BigInt(MAX_LIMIT)) {
728
+ throw invalidSyntax(`LIMIT ${value.raw} is out of range`, value.range);
729
+ }
730
+ return Number(n);
731
+ }
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);
738
+ }
739
+ if (value.parsed.length === 0) {
740
+ throw invalidSyntax('CURSOR must be a non-empty quoted pagination token', value.range);
741
+ }
742
+ return value.parsed;
743
+ }
744
+ function lowerMetadata(block) {
745
+ return block ? lowerJsonEntries(block.entries) : null;
746
+ }
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) {
759
+ 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);
773
+ }
774
+ }
775
+ /**
776
+ * Reads a literal in matcher or FILTER position.
777
+ *
778
+ * Stricter than {@link lowerJson}: these positions are not JSON-value
779
+ * positions in the grammar, and a trailing comma that an attribute block
780
+ * tolerates is a syntax error inside `IN [...]`.
781
+ */
782
+ function lowerKipValue(expr) {
783
+ if ((expr.kind === 'ArrayLiteral' || expr.kind === 'ObjectLiteral') &&
784
+ expr.trailingComma) {
785
+ throw invalidSyntax('a literal list takes no trailing comma', expr.range);
786
+ }
787
+ switch (expr.kind) {
788
+ case 'StringLiteral':
789
+ return { String: expr.parsed };
790
+ case 'NumberLiteral':
791
+ return { Number: numberValue(expr.value, expr.raw, expr.range) };
792
+ case 'BooleanLiteral':
793
+ return { Bool: expr.value };
794
+ case 'NullLiteral':
795
+ return 'Null';
796
+ case 'ArrayLiteral':
797
+ return { Array: expr.elements.map(lowerKipValue) };
798
+ case 'ObjectLiteral': {
799
+ 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
+ }
804
+ out[entry.key] = lowerKipValue(entry.value);
805
+ }
806
+ return { Object: out };
807
+ }
808
+ default:
809
+ throw invalidSyntax(`expected a literal value, found ${describeExpression(expr)}`, expr.range);
810
+ }
811
+ }
812
+ /**
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.
821
+ */
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);
855
+ }
856
+ return { var: varName(node.name, node.range), path };
857
+ }
858
+ /** Strips the `?` sigil; the executable form carries bare names. */
859
+ function varName(name, range) {
860
+ if (!name.startsWith('?')) {
861
+ throw invalidSyntax(`expected a variable, found ${name}`, range);
862
+ }
863
+ return name.slice(1);
864
+ }
865
+ function describeExpression(expr) {
866
+ switch (expr.kind) {
867
+ case 'ParameterRef':
868
+ return `the parameter ${expr.name} (this engine does not substitute parameters)`;
869
+ case 'VariableRef':
870
+ return `the variable ${expr.name}`;
871
+ case 'FunctionCallExpr':
872
+ return `a call to ${expr.name}`;
873
+ default:
874
+ return expr.kind;
875
+ }
876
+ }
877
+ //# sourceMappingURL=lower.js.map