@ldclabs/kip-lang 0.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -44
  4. package/dist/ast.d.ts +519 -144
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/budget.d.ts +37 -0
  7. package/dist/budget.d.ts.map +1 -0
  8. package/dist/budget.js +105 -0
  9. package/dist/budget.js.map +1 -0
  10. package/dist/diagnostics.d.ts +8 -2
  11. package/dist/diagnostics.d.ts.map +1 -1
  12. package/dist/diagnostics.js +32 -3
  13. package/dist/diagnostics.js.map +1 -1
  14. package/dist/errors.d.ts +29 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/exec-ast.d.ts +679 -0
  19. package/dist/exec-ast.d.ts.map +1 -0
  20. package/dist/exec-ast.js +24 -0
  21. package/dist/exec-ast.js.map +1 -0
  22. package/dist/formatter.d.ts.map +1 -1
  23. package/dist/formatter.js +870 -479
  24. package/dist/formatter.js.map +1 -1
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/lexer.d.ts.map +1 -1
  30. package/dist/lexer.js +46 -33
  31. package/dist/lexer.js.map +1 -1
  32. package/dist/lower.d.ts +17 -0
  33. package/dist/lower.d.ts.map +1 -0
  34. package/dist/lower.js +1566 -0
  35. package/dist/lower.js.map +1 -0
  36. package/dist/parser.d.ts.map +1 -1
  37. package/dist/parser.js +2476 -1152
  38. package/dist/parser.js.map +1 -1
  39. package/dist/semantics.d.ts +11 -7
  40. package/dist/semantics.d.ts.map +1 -1
  41. package/dist/semantics.js +295 -180
  42. package/dist/semantics.js.map +1 -1
  43. package/dist/token.d.ts +130 -40
  44. package/dist/token.d.ts.map +1 -1
  45. package/dist/token.js +264 -83
  46. package/dist/token.js.map +1 -1
  47. package/dist/version.d.ts +13 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +13 -0
  50. package/dist/version.js.map +1 -0
  51. package/package.json +36 -6
  52. package/src/ast.ts +914 -0
  53. package/src/budget.ts +108 -0
  54. package/src/diagnostics.ts +182 -0
  55. package/src/errors.ts +42 -0
  56. package/src/exec-ast.ts +614 -0
  57. package/src/formatter.ts +1339 -0
  58. package/src/index.ts +226 -0
  59. package/src/lexer.ts +459 -0
  60. package/src/lower.ts +2011 -0
  61. package/src/parser.ts +3506 -0
  62. package/src/semantics.ts +392 -0
  63. package/src/token.ts +408 -0
  64. package/src/version.ts +13 -0
@@ -0,0 +1,1339 @@
1
+ import { tokenize } from './lexer.js'
2
+ import { parse } from './parser.js'
3
+ import { diagnose } from './diagnostics.js'
4
+ import { Token, TokenType } from './token.js'
5
+ import type {
6
+ BaseNode,
7
+ Program,
8
+ Statement,
9
+ MutationClause,
10
+ FindStatement,
11
+ AsOfClause,
12
+ OrderByClause,
13
+ LimitClause,
14
+ CursorClause,
15
+ WhereClause,
16
+ WherePattern,
17
+ PropositionTuple,
18
+ Term,
19
+ RawPredicateExpression,
20
+ PredicateAtom,
21
+ ObjectPattern,
22
+ MutateStatement,
23
+ CreateConceptStatement,
24
+ UpsertConceptStatement,
25
+ EnsurePropositionStatement,
26
+ AssertStatement,
27
+ CreateEvidenceStatement,
28
+ CreateAssertionStatement,
29
+ CreateActivityStatement,
30
+ SetFacetClause,
31
+ SetStructuralClause,
32
+ UnsetStructuralClause,
33
+ UnsetAttributesClause,
34
+ UnsetFacetClause,
35
+ UpdateStatement,
36
+ UpdateAction,
37
+ RetractAssertionStatement,
38
+ SupersedeAssertionStatement,
39
+ CorrectEvidenceStatement,
40
+ TransitionActivityStatement,
41
+ SetRetentionStatement,
42
+ ArchiveStatement,
43
+ TombstoneStatement,
44
+ PurgeStatement,
45
+ MergeConceptStatement,
46
+ DescribeStatement,
47
+ ListStatement,
48
+ SearchStatement,
49
+ VerifyStatement,
50
+ ValidateStatement,
51
+ PreviewStatement,
52
+ HistoryStatement,
53
+ ChangesStatement,
54
+ SnapshotStatement,
55
+ ExportCapsuleStatement,
56
+ Expression,
57
+ ScalarValue,
58
+ SchemaSymbol,
59
+ TargetRef,
60
+ ObjectEntry,
61
+ ObjectLiteral
62
+ } from './ast.js'
63
+
64
+ export interface FormatOptions {
65
+ /** Number of spaces per indentation level (default: 4) */
66
+ indentSize?: number
67
+ /**
68
+ * Alphabetically sort keys inside `SET ATTRIBUTES` (default: false).
69
+ * Author key order is preserved by default; sorting is skipped for any block
70
+ * that contains comments, since reordering would detach a comment from its key.
71
+ */
72
+ sortAttributes?: boolean
73
+ }
74
+
75
+ export function format(source: string, options?: FormatOptions): string {
76
+ const opts: Required<FormatOptions> = {
77
+ indentSize: options?.indentSize ?? 4,
78
+ sortAttributes: options?.sortAttributes ?? false
79
+ }
80
+
81
+ const firstError = diagnose(source).find((d) => d.severity === 'error')
82
+ if (firstError) {
83
+ throw new Error(`Cannot format invalid KIP: ${firstError.message}`)
84
+ }
85
+
86
+ const tokens = tokenize(source)
87
+ const { ast } = parse(source)
88
+
89
+ const formatter = new Formatter(opts, tokens)
90
+ return formatter.formatProgram(ast)
91
+ }
92
+
93
+ interface CommentInfo {
94
+ line: number
95
+ column: number
96
+ text: string
97
+ }
98
+
99
+ class Formatter {
100
+ private opts: Required<FormatOptions>
101
+ private comments: CommentInfo[]
102
+ private commentIdx: number = 0
103
+ private output: string = ''
104
+ private indentLevel: number = 0
105
+
106
+ constructor(opts: Required<FormatOptions>, tokens: Token[]) {
107
+ this.opts = opts
108
+ this.comments = tokens
109
+ .filter((t) => t.type === TokenType.Comment)
110
+ .map((t) => ({ line: t.line, column: t.column, text: t.value }))
111
+ }
112
+
113
+ /** Emit all comments whose source line < beforeLine */
114
+ private emitCommentsBefore(beforeLine: number): void {
115
+ while (
116
+ this.commentIdx < this.comments.length &&
117
+ this.comments[this.commentIdx]!.line < beforeLine
118
+ ) {
119
+ this.writeIndent()
120
+ this.write(this.comments[this.commentIdx]!.text)
121
+ this.newline()
122
+ this.commentIdx++
123
+ }
124
+ }
125
+
126
+ private emitRemainingComments(): void {
127
+ while (this.commentIdx < this.comments.length) {
128
+ this.writeIndent()
129
+ this.write(this.comments[this.commentIdx]!.text)
130
+ this.newline()
131
+ this.commentIdx++
132
+ }
133
+ }
134
+
135
+ /**
136
+ * True if an un-emitted comment lies within a node spanning
137
+ * [startLine, endLine]. Used to keep a block multi-line so interior
138
+ * comments can be preserved at their position rather than relocated.
139
+ */
140
+ private hasPendingCommentInRange(startLine: number, endLine: number): boolean {
141
+ for (let i = this.commentIdx; i < this.comments.length; i++) {
142
+ const line = this.comments[i]!.line
143
+ if (line > endLine) break
144
+ if (line >= startLine) return true
145
+ }
146
+ return false
147
+ }
148
+
149
+ formatProgram(program: Program): string {
150
+ this.output = ''
151
+
152
+ for (let i = 0; i < program.statements.length; i++) {
153
+ const stmt = program.statements[i]!
154
+ // Separate statements with exactly one blank line.
155
+ if (i > 0) this.newline()
156
+ this.emitCommentsBefore(stmt.range.start.line)
157
+ this.formatStatement(stmt)
158
+ }
159
+
160
+ this.emitRemainingComments()
161
+ return this.output.trimEnd() + '\n'
162
+ }
163
+
164
+ // ────────────────────────────────────────────────────────────────────
165
+ // Statement dispatch
166
+ // ────────────────────────────────────────────────────────────────────
167
+
168
+ private formatStatement(stmt: Statement): void {
169
+ switch (stmt.kind) {
170
+ case 'FindStatement':
171
+ this.formatFind(stmt)
172
+ break
173
+ case 'MutateStatement':
174
+ this.formatMutate(stmt)
175
+ break
176
+ case 'DescribeStatement':
177
+ this.formatDescribe(stmt)
178
+ break
179
+ case 'ListStatement':
180
+ this.formatList(stmt)
181
+ break
182
+ case 'SearchStatement':
183
+ this.formatSearch(stmt)
184
+ break
185
+ case 'VerifyStatement':
186
+ this.formatVerify(stmt)
187
+ break
188
+ case 'ValidateStatement':
189
+ this.formatValidate(stmt)
190
+ break
191
+ case 'PreviewStatement':
192
+ this.formatPreview(stmt)
193
+ break
194
+ case 'HistoryStatement':
195
+ this.formatHistory(stmt)
196
+ break
197
+ case 'ChangesStatement':
198
+ this.formatChanges(stmt)
199
+ break
200
+ case 'SnapshotStatement':
201
+ this.formatSnapshot(stmt)
202
+ break
203
+ case 'ExportCapsuleStatement':
204
+ this.formatExport(stmt)
205
+ break
206
+ default:
207
+ this.formatMutationClause(stmt)
208
+ }
209
+ }
210
+
211
+ private formatMutationClause(stmt: MutationClause): void {
212
+ switch (stmt.kind) {
213
+ case 'CreateConceptStatement':
214
+ this.formatCreateConcept(stmt)
215
+ break
216
+ case 'UpsertConceptStatement':
217
+ this.formatUpsertConcept(stmt)
218
+ break
219
+ case 'EnsurePropositionStatement':
220
+ this.formatEnsureProposition(stmt)
221
+ break
222
+ case 'AssertStatement':
223
+ this.formatAssert(stmt)
224
+ break
225
+ case 'CreateEvidenceStatement':
226
+ this.formatRecordCreate('EVIDENCE', stmt)
227
+ break
228
+ case 'CreateAssertionStatement':
229
+ this.formatRecordCreate('ASSERTION', stmt)
230
+ break
231
+ case 'CreateActivityStatement':
232
+ this.formatRecordCreate('ACTIVITY', stmt)
233
+ break
234
+ case 'UpdateStatement':
235
+ this.formatUpdate(stmt)
236
+ break
237
+ case 'RetractAssertionStatement':
238
+ this.formatRetract(stmt)
239
+ break
240
+ case 'SupersedeAssertionStatement':
241
+ this.formatSupersede(stmt)
242
+ break
243
+ case 'CorrectEvidenceStatement':
244
+ this.formatCorrect(stmt)
245
+ break
246
+ case 'TransitionActivityStatement':
247
+ this.formatTransition(stmt)
248
+ break
249
+ case 'SetRetentionStatement':
250
+ this.formatSetRetention(stmt)
251
+ break
252
+ case 'ArchiveStatement':
253
+ this.formatRemoval('ARCHIVE', stmt)
254
+ break
255
+ case 'TombstoneStatement':
256
+ this.formatRemoval('TOMBSTONE', stmt)
257
+ break
258
+ case 'PurgeStatement':
259
+ this.formatPurge(stmt)
260
+ break
261
+ case 'MergeConceptStatement':
262
+ this.formatMerge(stmt)
263
+ break
264
+ }
265
+ }
266
+
267
+ // ────────────────────────────────────────────────────────────────────
268
+ // KQL
269
+ // ────────────────────────────────────────────────────────────────────
270
+
271
+ private formatFind(stmt: FindStatement): void {
272
+ this.writeIndent()
273
+ this.write('FIND(')
274
+ this.write(stmt.projections.map((p) => this.expr(p)).join(', '))
275
+ this.write(')')
276
+ this.newline()
277
+
278
+ this.formatWhere(stmt.where, 'WHERE')
279
+
280
+ if (stmt.asOf) {
281
+ this.writeIndent()
282
+ this.write(this.asOfToString(stmt.asOf))
283
+ this.newline()
284
+ }
285
+ if (stmt.forTime) {
286
+ this.writeIndent()
287
+ this.write(`FOR TIME ${this.scalar(stmt.forTime.value)}`)
288
+ this.newline()
289
+ }
290
+ if (stmt.epistemic) {
291
+ this.writeIndent()
292
+ this.write('WITH EPISTEMIC ')
293
+ this.formatObjectBlock(stmt.epistemic.options, false)
294
+ this.newline()
295
+ }
296
+ if (stmt.orderBy) this.formatOrderBy(stmt.orderBy)
297
+ if (stmt.limit) this.formatLimit(stmt.limit)
298
+ if (stmt.cursor) this.formatCursor(stmt.cursor)
299
+ }
300
+
301
+ private formatOrderBy(clause: OrderByClause): void {
302
+ this.writeIndent()
303
+ this.write('ORDER BY ')
304
+ this.write(
305
+ clause.items
306
+ .map((item) => {
307
+ const expr = this.expr(item.expression)
308
+ return item.direction ? `${expr} ${item.direction}` : expr
309
+ })
310
+ .join(', ')
311
+ )
312
+ this.newline()
313
+ }
314
+
315
+ private formatLimit(clause: LimitClause): void {
316
+ this.writeIndent()
317
+ this.write(`LIMIT ${this.scalar(clause.value)}`)
318
+ this.newline()
319
+ }
320
+
321
+ private formatCursor(clause: CursorClause): void {
322
+ this.writeIndent()
323
+ this.write(`CURSOR ${this.scalar(clause.value)}`)
324
+ this.newline()
325
+ }
326
+
327
+ private asOfToString(clause: AsOfClause): string {
328
+ return `AS OF ${clause.basis} ${this.scalar(clause.value)}`
329
+ }
330
+
331
+ // ────────────────────────────────────────────────────────────────────
332
+ // WHERE
333
+ // ────────────────────────────────────────────────────────────────────
334
+
335
+ private formatWhere(clause: WhereClause, keyword: string): void {
336
+ this.writeIndent()
337
+ this.write(`${keyword} {`)
338
+ this.newline()
339
+ this.indentLevel++
340
+ for (const pattern of clause.patterns) {
341
+ this.emitCommentsBefore(pattern.range.start.line)
342
+ this.formatWherePattern(pattern)
343
+ }
344
+ this.emitCommentsBefore(clause.range.end.line)
345
+ this.indentLevel--
346
+ this.writeIndent()
347
+ this.write('}')
348
+ this.newline()
349
+ }
350
+
351
+ private formatNestedBlock(
352
+ keyword: string,
353
+ patterns: WherePattern[],
354
+ endLine: number
355
+ ): void {
356
+ this.writeIndent()
357
+ this.write(`${keyword} {`)
358
+ this.newline()
359
+ this.indentLevel++
360
+ for (const pattern of patterns) {
361
+ this.emitCommentsBefore(pattern.range.start.line)
362
+ this.formatWherePattern(pattern)
363
+ }
364
+ this.emitCommentsBefore(endLine)
365
+ this.indentLevel--
366
+ this.writeIndent()
367
+ this.write('}')
368
+ this.newline()
369
+ }
370
+
371
+ private formatWherePattern(pattern: WherePattern): void {
372
+ switch (pattern.kind) {
373
+ case 'ConceptPattern':
374
+ this.writeIndent()
375
+ this.write(pattern.variable.name)
376
+ if (pattern.explicit) this.write(' CONCEPT')
377
+ this.write(' ')
378
+ this.write(this.objectPatternToString(pattern.matcher))
379
+ this.newline()
380
+ break
381
+
382
+ case 'PropositionPattern':
383
+ this.writeIndent()
384
+ if (pattern.variable) this.write(`${pattern.variable.name} `)
385
+ if (pattern.explicit) this.write('PROPOSITION ')
386
+ this.write(this.tupleToString(pattern.tuple))
387
+ this.newline()
388
+ break
389
+
390
+ case 'AssertionPattern':
391
+ case 'EvidencePattern':
392
+ case 'ActivityPattern': {
393
+ const keyword = {
394
+ AssertionPattern: 'ASSERTION',
395
+ EvidencePattern: 'EVIDENCE',
396
+ ActivityPattern: 'ACTIVITY'
397
+ }[pattern.kind]
398
+ this.writeIndent()
399
+ this.write(`${pattern.variable.name} ${keyword} `)
400
+ this.write(this.objectPatternToString(pattern.matcher))
401
+ this.newline()
402
+ break
403
+ }
404
+
405
+ case 'StructuralPattern':
406
+ this.writeIndent()
407
+ if (pattern.variable) this.write(`${pattern.variable.name} `)
408
+ this.write(
409
+ `STRUCTURAL (${this.term(pattern.subject)}, ${this.symbol(pattern.field)}, ${this.term(pattern.object)})`
410
+ )
411
+ this.newline()
412
+ break
413
+
414
+ case 'BeliefPattern':
415
+ this.writeIndent()
416
+ this.write(`${pattern.variable.name} BELIEF (`)
417
+ if (pattern.proposition) {
418
+ this.write(pattern.proposition.name)
419
+ } else if (pattern.propositionId) {
420
+ this.write(`id: ${this.scalar(pattern.propositionId)}`)
421
+ } else {
422
+ this.write(
423
+ `${this.term(pattern.subject!)}, ${this.predAtom(pattern.predicate!)}, ${this.term(pattern.object!)}`
424
+ )
425
+ }
426
+ this.write(')')
427
+ this.newline()
428
+ break
429
+
430
+ case 'BeliefSlotPattern':
431
+ this.writeIndent()
432
+ this.write(
433
+ `${pattern.variable.name} BELIEF SLOT (${this.term(pattern.subject)}, ${this.predAtom(pattern.predicate)})`
434
+ )
435
+ this.newline()
436
+ break
437
+
438
+ case 'FilterClause':
439
+ this.writeIndent()
440
+ this.write(`FILTER(${this.expr(pattern.expression)})`)
441
+ this.newline()
442
+ break
443
+
444
+ case 'NotClause':
445
+ this.formatNestedBlock('NOT', pattern.patterns, pattern.range.end.line)
446
+ break
447
+
448
+ case 'OptionalClause':
449
+ this.formatNestedBlock(
450
+ 'OPTIONAL',
451
+ pattern.patterns,
452
+ pattern.range.end.line
453
+ )
454
+ break
455
+
456
+ case 'UnionClause':
457
+ this.formatNestedBlock('UNION', pattern.patterns, pattern.range.end.line)
458
+ break
459
+ }
460
+ }
461
+
462
+ // ────────────────────────────────────────────────────────────────────
463
+ // KML
464
+ // ────────────────────────────────────────────────────────────────────
465
+
466
+ private formatMutate(stmt: MutateStatement): void {
467
+ this.writeIndent()
468
+ this.write('MUTATE {')
469
+ this.newline()
470
+ this.indentLevel++
471
+ for (let i = 0; i < stmt.clauses.length; i++) {
472
+ const clause = stmt.clauses[i]!
473
+ if (i > 0) this.newline()
474
+ this.emitCommentsBefore(clause.range.start.line)
475
+ this.formatMutationClause(clause)
476
+ }
477
+ this.emitCommentsBefore(stmt.range.end.line)
478
+ this.indentLevel--
479
+ this.writeIndent()
480
+ this.write('}')
481
+ this.newline()
482
+ }
483
+
484
+ /**
485
+ * A body clause together with the printer for it. Bodies (CREATE / UPSERT
486
+ * CONCEPT, CREATE EVIDENCE|ASSERTION|ACTIVITY) hold their clauses in typed
487
+ * slots, so the source order has to be recovered from the ranges before
488
+ * printing — a comment written above a clause must come out above that
489
+ * same clause, and the comment cursor only moves forward.
490
+ */
491
+ private formatBody(
492
+ header: string,
493
+ endLine: number,
494
+ clauses: { node: BaseNode; print: () => void }[]
495
+ ): void {
496
+ this.writeIndent()
497
+ this.write(`${header} {`)
498
+ this.newline()
499
+ this.indentLevel++
500
+
501
+ const ordered = [...clauses].sort(
502
+ (a, b) =>
503
+ a.node.range.start.line - b.node.range.start.line ||
504
+ a.node.range.start.column - b.node.range.start.column
505
+ )
506
+ for (const clause of ordered) {
507
+ this.emitCommentsBefore(clause.node.range.start.line)
508
+ clause.print()
509
+ }
510
+ // Comments after the last clause still belong inside the braces.
511
+ this.emitCommentsBefore(endLine)
512
+
513
+ this.indentLevel--
514
+ this.writeIndent()
515
+ this.write('}')
516
+ this.newline()
517
+ }
518
+
519
+ private formatCreateConcept(stmt: CreateConceptStatement): void {
520
+ const clauses: { node: BaseNode; print: () => void }[] = []
521
+ if (stmt.type) {
522
+ const node = stmt.type
523
+ clauses.push({
524
+ node,
525
+ print: () => {
526
+ this.writeIndent()
527
+ this.write(`TYPE ${this.symbol(node.value)}`)
528
+ this.newline()
529
+ }
530
+ })
531
+ }
532
+ if (stmt.clientKey) {
533
+ const node = stmt.clientKey
534
+ clauses.push({
535
+ node,
536
+ print: () => {
537
+ this.writeIndent()
538
+ this.write(`CLIENT KEY ${this.scalar(node.value)}`)
539
+ this.newline()
540
+ }
541
+ })
542
+ }
543
+ if (stmt.name) {
544
+ const node = stmt.name
545
+ clauses.push({
546
+ node,
547
+ print: () => {
548
+ this.writeIndent()
549
+ this.write(`NAME ${this.scalar(node.value)}`)
550
+ this.newline()
551
+ }
552
+ })
553
+ }
554
+ if (stmt.setFields) {
555
+ const node = stmt.setFields
556
+ clauses.push({
557
+ node,
558
+ print: () => this.formatAssignmentClause('SET FIELDS', node.assignments, false)
559
+ })
560
+ }
561
+ if (stmt.setAttributes) {
562
+ const node = stmt.setAttributes
563
+ clauses.push({
564
+ node,
565
+ print: () =>
566
+ this.formatAssignmentClause(
567
+ 'SET ATTRIBUTES',
568
+ node.assignments,
569
+ this.opts.sortAttributes
570
+ )
571
+ })
572
+ }
573
+ for (const facet of stmt.setFacets) {
574
+ clauses.push({ node: facet, print: () => this.formatFacet(facet) })
575
+ }
576
+ if (stmt.setStructural) {
577
+ const node = stmt.setStructural
578
+ clauses.push({ node, print: () => this.formatStructural(node) })
579
+ }
580
+ this.formatBody(
581
+ `CREATE CONCEPT ${stmt.handle.name}`,
582
+ stmt.range.end.line,
583
+ clauses
584
+ )
585
+ }
586
+
587
+ private formatUpsertConcept(stmt: UpsertConceptStatement): void {
588
+ const clauses: { node: BaseNode; print: () => void }[] = []
589
+ if (stmt.match) {
590
+ const node = stmt.match
591
+ clauses.push({
592
+ node,
593
+ print: () => {
594
+ this.writeIndent()
595
+ this.write('MATCH ')
596
+ this.write(this.objectPatternToString(node.pattern))
597
+ this.newline()
598
+ }
599
+ })
600
+ }
601
+ if (stmt.expectVersion) {
602
+ const node = stmt.expectVersion
603
+ clauses.push({
604
+ node,
605
+ print: () => {
606
+ this.writeIndent()
607
+ this.write(`EXPECT VERSION ${this.scalar(node.value)}`)
608
+ this.newline()
609
+ }
610
+ })
611
+ }
612
+ if (stmt.setFields) {
613
+ const node = stmt.setFields
614
+ clauses.push({
615
+ node,
616
+ print: () => this.formatAssignmentClause('SET FIELDS', node.assignments, false)
617
+ })
618
+ }
619
+ if (stmt.setAttributes) {
620
+ const node = stmt.setAttributes
621
+ clauses.push({
622
+ node,
623
+ print: () =>
624
+ this.formatAssignmentClause(
625
+ 'SET ATTRIBUTES',
626
+ node.assignments,
627
+ this.opts.sortAttributes
628
+ )
629
+ })
630
+ }
631
+ for (const facet of stmt.setFacets) {
632
+ clauses.push({ node: facet, print: () => this.formatFacet(facet) })
633
+ }
634
+ if (stmt.unsetAttributes) {
635
+ const node = stmt.unsetAttributes
636
+ clauses.push({ node, print: () => this.formatUnsetAttributes(node) })
637
+ }
638
+ for (const facet of stmt.unsetFacets) {
639
+ clauses.push({ node: facet, print: () => this.formatUnsetFacet(facet) })
640
+ }
641
+ if (stmt.setStructural) {
642
+ const node = stmt.setStructural
643
+ clauses.push({ node, print: () => this.formatStructural(node) })
644
+ }
645
+ if (stmt.unsetStructural) {
646
+ const node = stmt.unsetStructural
647
+ clauses.push({ node, print: () => this.formatUnsetStructural(node) })
648
+ }
649
+ this.formatBody(
650
+ `UPSERT CONCEPT ${stmt.handle.name}`,
651
+ stmt.range.end.line,
652
+ clauses
653
+ )
654
+ }
655
+
656
+ private formatRecordCreate(
657
+ keyword: string,
658
+ stmt:
659
+ | CreateEvidenceStatement
660
+ | CreateAssertionStatement
661
+ | CreateActivityStatement
662
+ ): void {
663
+ const clauses: { node: BaseNode; print: () => void }[] = []
664
+ if (stmt.clientKey) {
665
+ const node = stmt.clientKey
666
+ clauses.push({
667
+ node,
668
+ print: () => {
669
+ this.writeIndent()
670
+ this.write(`CLIENT KEY ${this.scalar(node.value)}`)
671
+ this.newline()
672
+ }
673
+ })
674
+ }
675
+ if (stmt.setFields) {
676
+ const node = stmt.setFields
677
+ clauses.push({
678
+ node,
679
+ print: () => this.formatAssignmentClause('SET FIELDS', node.assignments, false)
680
+ })
681
+ }
682
+ for (const facet of stmt.setFacets) {
683
+ clauses.push({ node: facet, print: () => this.formatFacet(facet) })
684
+ }
685
+ if (stmt.setStructural) {
686
+ const node = stmt.setStructural
687
+ clauses.push({ node, print: () => this.formatStructural(node) })
688
+ }
689
+ this.formatBody(
690
+ `CREATE ${keyword} ${stmt.handle.name}`,
691
+ stmt.range.end.line,
692
+ clauses
693
+ )
694
+ }
695
+
696
+ private formatEnsureProposition(stmt: EnsurePropositionStatement): void {
697
+ this.writeIndent()
698
+ this.write('ENSURE PROPOSITION ')
699
+ if (stmt.handle) this.write(`${stmt.handle.name} `)
700
+ this.write(this.tupleToString(stmt.tuple))
701
+ if (stmt.expectVersion) {
702
+ this.write(` EXPECT VERSION ${this.scalar(stmt.expectVersion.value)}`)
703
+ }
704
+ this.newline()
705
+ }
706
+
707
+ private formatAssert(stmt: AssertStatement): void {
708
+ this.writeIndent()
709
+ this.write('ASSERT ')
710
+ if (stmt.handle) this.write(`${stmt.handle.name} `)
711
+ this.write(this.tupleToString(stmt.tuple))
712
+ this.write(' ')
713
+ this.formatObjectBlock(stmt.assignments, false)
714
+ if (stmt.superseding) {
715
+ this.newline()
716
+ this.indentLevel++
717
+ this.writeIndent()
718
+ this.write(`SUPERSEDING ${this.targetRef(stmt.superseding)}`)
719
+ this.indentLevel--
720
+ }
721
+ this.newline()
722
+ }
723
+
724
+ private formatUpdate(stmt: UpdateStatement): void {
725
+ this.writeIndent()
726
+ this.write(`UPDATE ${this.targetRef(stmt.target)}`)
727
+ this.newline()
728
+
729
+ if (stmt.expectVersion) {
730
+ this.emitCommentsBefore(stmt.expectVersion.range.start.line)
731
+ this.writeIndent()
732
+ this.write(`EXPECT VERSION ${this.scalar(stmt.expectVersion.value)}`)
733
+ this.newline()
734
+ }
735
+ for (const action of stmt.actions) {
736
+ this.emitCommentsBefore(action.range.start.line)
737
+ this.formatUpdateAction(action)
738
+ }
739
+ if (stmt.where) {
740
+ this.emitCommentsBefore(stmt.where.range.start.line)
741
+ this.formatWhere(stmt.where, 'WHERE')
742
+ }
743
+ if (stmt.limit) {
744
+ this.emitCommentsBefore(stmt.limit.range.start.line)
745
+ this.formatLimit(stmt.limit)
746
+ }
747
+ }
748
+
749
+ private formatUpdateAction(action: UpdateAction): void {
750
+ switch (action.kind) {
751
+ case 'SetFieldsClause':
752
+ this.formatAssignmentClause('SET FIELDS', action.assignments, false)
753
+ break
754
+ case 'SetAttributesClause':
755
+ this.formatAssignmentClause(
756
+ 'SET ATTRIBUTES',
757
+ action.assignments,
758
+ this.opts.sortAttributes
759
+ )
760
+ break
761
+ case 'SetFacetClause':
762
+ this.formatFacet(action)
763
+ break
764
+ case 'UnsetAttributesClause':
765
+ this.formatUnsetAttributes(action)
766
+ break
767
+ case 'UnsetFacetClause':
768
+ this.formatUnsetFacet(action)
769
+ break
770
+ case 'SetStructuralClause':
771
+ this.formatStructural(action)
772
+ break
773
+ case 'UnsetStructuralClause':
774
+ this.formatUnsetStructural(action)
775
+ break
776
+ }
777
+ }
778
+
779
+ private formatRetract(stmt: RetractAssertionStatement): void {
780
+ this.writeIndent()
781
+ this.write(`RETRACT ASSERTION ${this.targetRef(stmt.target)}`)
782
+ if (!stmt.where && !stmt.limit && stmt.expectState) {
783
+ this.write(` EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
784
+ this.newline()
785
+ return
786
+ }
787
+ this.newline()
788
+ if (stmt.where) this.formatWhere(stmt.where, 'WHERE')
789
+ if (stmt.limit) this.formatLimit(stmt.limit)
790
+ if (stmt.expectState) {
791
+ this.writeIndent()
792
+ this.write(`EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
793
+ this.newline()
794
+ }
795
+ }
796
+
797
+ private formatSupersede(stmt: SupersedeAssertionStatement): void {
798
+ this.writeIndent()
799
+ this.write(
800
+ `SUPERSEDE ASSERTION ${this.targetRef(stmt.target)} BY ${this.targetRef(stmt.by)}`
801
+ )
802
+ if (stmt.expectState) {
803
+ this.write(` EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
804
+ }
805
+ this.newline()
806
+ }
807
+
808
+ private formatCorrect(stmt: CorrectEvidenceStatement): void {
809
+ this.writeIndent()
810
+ this.write(
811
+ `CORRECT EVIDENCE ${this.targetRef(stmt.target)} BY ${this.targetRef(stmt.by)}`
812
+ )
813
+ if (stmt.expectState) {
814
+ this.write(` EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
815
+ }
816
+ this.newline()
817
+ }
818
+
819
+ private formatTransition(stmt: TransitionActivityStatement): void {
820
+ this.writeIndent()
821
+ this.write(
822
+ `TRANSITION ACTIVITY ${this.targetRef(stmt.target)} TO ${this.scalar(stmt.to)}`
823
+ )
824
+ this.newline()
825
+ if (stmt.finalize.length > 0) {
826
+ this.indentLevel++
827
+ for (const clause of stmt.finalize) {
828
+ this.emitCommentsBefore(clause.range.start.line)
829
+ if (clause.kind === 'SetFieldsClause') {
830
+ this.formatAssignmentClause('SET FIELDS', clause.assignments, false)
831
+ } else {
832
+ this.formatStructural(clause)
833
+ }
834
+ }
835
+ this.indentLevel--
836
+ }
837
+ if (stmt.expectState) {
838
+ this.indentLevel++
839
+ this.emitCommentsBefore(stmt.expectState.range.start.line)
840
+ this.writeIndent()
841
+ this.write(`EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
842
+ this.newline()
843
+ this.indentLevel--
844
+ }
845
+ }
846
+
847
+ private formatSetRetention(stmt: SetRetentionStatement): void {
848
+ this.writeIndent()
849
+ this.write(`SET RETENTION ${this.targetRef(stmt.target)} `)
850
+ this.formatObjectBlock(stmt.assignments, false)
851
+ this.newline()
852
+ if (stmt.where) this.formatWhere(stmt.where, 'WHERE')
853
+ if (stmt.limit) this.formatLimit(stmt.limit)
854
+ if (stmt.expectVersion) {
855
+ this.writeIndent()
856
+ this.write(`EXPECT VERSION ${this.scalar(stmt.expectVersion.value)}`)
857
+ this.newline()
858
+ }
859
+ }
860
+
861
+ private formatRemoval(
862
+ keyword: string,
863
+ stmt: ArchiveStatement | TombstoneStatement
864
+ ): void {
865
+ this.writeIndent()
866
+ this.write(`${keyword} ${this.targetRef(stmt.target)}`)
867
+ if (!stmt.where && !stmt.limit) {
868
+ if (stmt.expectState) {
869
+ this.write(` EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
870
+ }
871
+ this.newline()
872
+ return
873
+ }
874
+ this.newline()
875
+ if (stmt.where) this.formatWhere(stmt.where, 'WHERE')
876
+ if (stmt.limit) this.formatLimit(stmt.limit)
877
+ if (stmt.expectState) {
878
+ this.writeIndent()
879
+ this.write(`EXPECT STATE ${this.scalar(stmt.expectState.value)}`)
880
+ this.newline()
881
+ }
882
+ }
883
+
884
+ private formatPurge(stmt: PurgeStatement): void {
885
+ this.writeIndent()
886
+ this.write(`PURGE ${this.targetRef(stmt.target)}`)
887
+ this.newline()
888
+ if (stmt.where) this.formatWhere(stmt.where, 'WHERE')
889
+ if (stmt.limit) this.formatLimit(stmt.limit)
890
+ this.indentLevel++
891
+ if (stmt.referencePolicy) {
892
+ this.writeIndent()
893
+ this.write(`REFERENCE POLICY ${this.scalar(stmt.referencePolicy)}`)
894
+ this.newline()
895
+ }
896
+ this.writeIndent()
897
+ this.write(`CONFIRM ${stmt.confirm.value}`)
898
+ this.newline()
899
+ this.indentLevel--
900
+ }
901
+
902
+ private formatMerge(stmt: MergeConceptStatement): void {
903
+ this.writeIndent()
904
+ this.write(
905
+ `MERGE CONCEPT ${this.targetRef(stmt.source)} INTO ${this.targetRef(stmt.into)}`
906
+ )
907
+ this.newline()
908
+ if (stmt.where) this.formatWhere(stmt.where, 'WHERE')
909
+ if (stmt.expectVersion) {
910
+ this.writeIndent()
911
+ this.write(`EXPECT VERSION ${this.scalar(stmt.expectVersion.value)}`)
912
+ this.newline()
913
+ }
914
+ }
915
+
916
+ // ────────────────────────────────────────────────────────────────────
917
+ // KML clause bodies
918
+ // ────────────────────────────────────────────────────────────────────
919
+
920
+ private formatAssignmentClause(
921
+ keyword: string,
922
+ assignments: ObjectLiteral,
923
+ sort: boolean
924
+ ): void {
925
+ this.writeIndent()
926
+ this.write(`${keyword} `)
927
+ this.formatObjectBlock(assignments, sort)
928
+ this.newline()
929
+ }
930
+
931
+ private formatFacet(clause: SetFacetClause): void {
932
+ this.writeIndent()
933
+ this.write(`SET FACET ${this.symbol(clause.facet)} `)
934
+ this.formatObjectBlock(clause.assignments, false)
935
+ this.newline()
936
+ }
937
+
938
+ private formatUnsetAttributes(clause: UnsetAttributesClause): void {
939
+ this.writeIndent()
940
+ this.write(
941
+ `UNSET ATTRIBUTES { ${clause.fields.map((f) => this.fieldName(f.name, f.isQuoted)).join(', ')} }`
942
+ )
943
+ this.newline()
944
+ }
945
+
946
+ private formatUnsetFacet(clause: UnsetFacetClause): void {
947
+ this.writeIndent()
948
+ this.write(
949
+ `UNSET FACET ${this.symbol(clause.facet)} { ${clause.fields.map((f) => this.fieldName(f.name, f.isQuoted)).join(', ')} }`
950
+ )
951
+ this.newline()
952
+ }
953
+
954
+ private formatStructural(clause: SetStructuralClause): void {
955
+ this.writeIndent()
956
+ this.write('SET STRUCTURAL {')
957
+ this.newline()
958
+ this.indentLevel++
959
+ for (const assignment of clause.assignments) {
960
+ this.emitCommentsBefore(assignment.range.start.line)
961
+ this.writeIndent()
962
+ this.write(
963
+ `(${this.symbol(assignment.field)}, ${this.expr(assignment.value)})`
964
+ )
965
+ if (assignment.options) {
966
+ this.write(` ${this.objectLiteralToString(assignment.options)}`)
967
+ }
968
+ this.newline()
969
+ }
970
+ this.emitCommentsBefore(clause.range.end.line)
971
+ this.indentLevel--
972
+ this.writeIndent()
973
+ this.write('}')
974
+ this.newline()
975
+ }
976
+
977
+ private formatUnsetStructural(clause: UnsetStructuralClause): void {
978
+ this.writeIndent()
979
+ this.write('UNSET STRUCTURAL {')
980
+ this.newline()
981
+ this.indentLevel++
982
+ for (const removal of clause.removals) {
983
+ this.emitCommentsBefore(removal.range.start.line)
984
+ this.writeIndent()
985
+ this.write(`(${this.symbol(removal.field)}, ${this.expr(removal.value)})`)
986
+ this.newline()
987
+ }
988
+ this.emitCommentsBefore(clause.range.end.line)
989
+ this.indentLevel--
990
+ this.writeIndent()
991
+ this.write('}')
992
+ this.newline()
993
+ }
994
+
995
+ // ────────────────────────────────────────────────────────────────────
996
+ // META
997
+ // ────────────────────────────────────────────────────────────────────
998
+
999
+ private formatDescribe(stmt: DescribeStatement): void {
1000
+ this.writeIndent()
1001
+ const words: Record<DescribeStatement['target'], string> = {
1002
+ PRIMER: 'PRIMER',
1003
+ PROTOCOL: 'PROTOCOL',
1004
+ EXECUTION_CONTEXT: 'EXECUTION CONTEXT',
1005
+ CAPABILITIES: 'CAPABILITIES',
1006
+ SPACE: 'SPACE',
1007
+ SCHEMA_ENVIRONMENT: 'SCHEMA ENVIRONMENT',
1008
+ PACKAGE: 'PACKAGE',
1009
+ TYPE: 'TYPE',
1010
+ PREDICATE: 'PREDICATE',
1011
+ FACET: 'FACET',
1012
+ STRUCTURAL_FIELD: 'STRUCTURAL FIELD',
1013
+ COMPATIBILITY: 'COMPATIBILITY',
1014
+ ERROR: 'ERROR',
1015
+ TRANSACTION: 'TRANSACTION',
1016
+ TRANSACTION_BY_IDEMPOTENCY_KEY: 'TRANSACTION BY IDEMPOTENCY KEY',
1017
+ SNAPSHOT: 'SNAPSHOT',
1018
+ CAPSULE: 'CAPSULE',
1019
+ EPISTEMIC_POLICY: 'EPISTEMIC POLICY',
1020
+ PROJECTION_CAPABILITY: 'PROJECTION CAPABILITY',
1021
+ TRUST: 'TRUST',
1022
+ ACCESS: 'ACCESS'
1023
+ }
1024
+ this.write(`DESCRIBE ${words[stmt.target]}`)
1025
+
1026
+ if (stmt.target === 'COMPATIBILITY' && stmt.from && stmt.to) {
1027
+ this.write(` FROM ${this.scalar(stmt.from)} TO ${this.scalar(stmt.to)}`)
1028
+ } else if (stmt.value) {
1029
+ this.write(` ${this.scalar(stmt.value)}`)
1030
+ }
1031
+ if (stmt.mode) this.write(` MODE ${this.scalar(stmt.mode)}`)
1032
+ if (stmt.asOf) this.write(` ${this.asOfToString(stmt.asOf)}`)
1033
+ if (stmt.with) this.write(` WITH ${this.objectLiteralToString(stmt.with)}`)
1034
+ this.newline()
1035
+ }
1036
+
1037
+ private formatList(stmt: ListStatement): void {
1038
+ this.writeIndent()
1039
+ const words: Record<ListStatement['target'], string> = {
1040
+ SPACES: 'SPACES',
1041
+ SCHEMA_PACKAGES: 'SCHEMA PACKAGES',
1042
+ TYPES: 'TYPES',
1043
+ PREDICATES: 'PREDICATES',
1044
+ FACETS: 'FACETS',
1045
+ STRUCTURAL_FIELDS: 'STRUCTURAL FIELDS',
1046
+ EPISTEMIC_POLICIES: 'EPISTEMIC POLICIES'
1047
+ }
1048
+ this.write(`LIST ${words[stmt.target]}`)
1049
+ if (stmt.status) this.write(` STATUS ${this.scalar(stmt.status)}`)
1050
+ if (stmt.limit) this.write(` LIMIT ${this.scalar(stmt.limit.value)}`)
1051
+ if (stmt.cursor) this.write(` CURSOR ${this.scalar(stmt.cursor.value)}`)
1052
+ this.newline()
1053
+ }
1054
+
1055
+ private formatSearch(stmt: SearchStatement): void {
1056
+ this.writeIndent()
1057
+ this.write(`SEARCH ${stmt.searchKind} ${this.scalar(stmt.term)}`)
1058
+ if (stmt.withType) this.write(` WITH TYPE ${this.scalar(stmt.withType)}`)
1059
+ if (stmt.withPredicate) {
1060
+ this.write(` WITH PREDICATE ${this.scalar(stmt.withPredicate)}`)
1061
+ }
1062
+ if (stmt.mode) this.write(` MODE ${this.scalar(stmt.mode)}`)
1063
+ if (stmt.threshold) this.write(` THRESHOLD ${this.scalar(stmt.threshold)}`)
1064
+ if (stmt.asOfSeq) this.write(` AS OF SEQ ${this.scalar(stmt.asOfSeq)}`)
1065
+ if (stmt.limit) this.write(` LIMIT ${this.scalar(stmt.limit.value)}`)
1066
+ if (stmt.cursor) this.write(` CURSOR ${this.scalar(stmt.cursor.value)}`)
1067
+ this.newline()
1068
+ }
1069
+
1070
+ private formatVerify(stmt: VerifyStatement): void {
1071
+ const words: Record<VerifyStatement['target'], string> = {
1072
+ CAPSULE: 'CAPSULE',
1073
+ SCHEMA_PACKAGE: 'SCHEMA PACKAGE',
1074
+ RECEIPT: 'RECEIPT',
1075
+ BLOB: 'BLOB',
1076
+ CHECKPOINT: 'CHECKPOINT'
1077
+ }
1078
+ this.writeIndent()
1079
+ this.write(`VERIFY ${words[stmt.target]} ${this.scalar(stmt.value)}`)
1080
+ this.newline()
1081
+ }
1082
+
1083
+ private formatValidate(stmt: ValidateStatement): void {
1084
+ const words: Record<ValidateStatement['target'], string> = {
1085
+ KQL: 'KQL',
1086
+ KML: 'KML',
1087
+ CAPSULE: 'CAPSULE',
1088
+ SCHEMA_PACKAGE: 'SCHEMA PACKAGE',
1089
+ IMPORT_PLAN: 'IMPORT PLAN'
1090
+ }
1091
+ this.writeIndent()
1092
+ this.write(`VALIDATE ${words[stmt.target]} ${this.scalar(stmt.value)}`)
1093
+ if (stmt.options) {
1094
+ this.write(` WITH ${this.objectLiteralToString(stmt.options)}`)
1095
+ }
1096
+ this.newline()
1097
+ }
1098
+
1099
+ private formatPreview(stmt: PreviewStatement): void {
1100
+ this.writeIndent()
1101
+ if (stmt.target === 'KML') {
1102
+ this.write(`PREVIEW KML ${this.scalar(stmt.value)}`)
1103
+ } else {
1104
+ this.write(
1105
+ `PREVIEW IMPORT CAPSULE ${this.scalar(stmt.value)} INTO ${this.scalar(stmt.into!)}`
1106
+ )
1107
+ }
1108
+ this.newline()
1109
+ }
1110
+
1111
+ private formatHistory(stmt: HistoryStatement): void {
1112
+ this.writeIndent()
1113
+ this.write(`HISTORY ${stmt.target}`)
1114
+ if (stmt.value) this.write(` ${this.scalar(stmt.value)}`)
1115
+ if (stmt.fromSeq) this.write(` FROM SEQ ${this.scalar(stmt.fromSeq)}`)
1116
+ if (stmt.toSeq) this.write(` TO SEQ ${this.scalar(stmt.toSeq)}`)
1117
+ if (stmt.limit) this.write(` LIMIT ${this.scalar(stmt.limit.value)}`)
1118
+ if (stmt.cursor) this.write(` CURSOR ${this.scalar(stmt.cursor.value)}`)
1119
+ this.newline()
1120
+ }
1121
+
1122
+ private formatChanges(stmt: ChangesStatement): void {
1123
+ this.writeIndent()
1124
+ const keyword = stmt.mode === 'SINCE' ? 'SINCE' : 'AFTER SEQ'
1125
+ this.write(`CHANGES ${keyword} ${this.scalar(stmt.value)}`)
1126
+ if (stmt.limit) this.write(` LIMIT ${this.scalar(stmt.limit.value)}`)
1127
+ this.newline()
1128
+ }
1129
+
1130
+ private formatSnapshot(stmt: SnapshotStatement): void {
1131
+ this.writeIndent()
1132
+ this.write('SNAPSHOT')
1133
+ if (stmt.asOf) this.write(` ${this.asOfToString(stmt.asOf)}`)
1134
+ this.newline()
1135
+ }
1136
+
1137
+ private formatExport(stmt: ExportCapsuleStatement): void {
1138
+ this.writeIndent()
1139
+ this.write(`EXPORT CAPSULE ${this.targetRef(stmt.target)}`)
1140
+ this.newline()
1141
+ this.formatWhere(stmt.where, 'WHERE')
1142
+ if (stmt.options) {
1143
+ this.writeIndent()
1144
+ this.write(`WITH ${this.objectLiteralToString(stmt.options)}`)
1145
+ this.newline()
1146
+ }
1147
+ if (stmt.asOf) {
1148
+ this.writeIndent()
1149
+ this.write(this.asOfToString(stmt.asOf))
1150
+ this.newline()
1151
+ }
1152
+ }
1153
+
1154
+ // ────────────────────────────────────────────────────────────────────
1155
+ // Objects
1156
+ // ────────────────────────────────────────────────────────────────────
1157
+
1158
+ /**
1159
+ * Emits `{...}` on one line when it is short and comment-free, otherwise
1160
+ * one entry per line. A block holding a comment always stays multi-line, so
1161
+ * the comment keeps the key it was written against.
1162
+ */
1163
+ private formatObjectBlock(object: ObjectLiteral, sort: boolean): void {
1164
+ const entries = object.entries
1165
+ if (entries.length === 0) {
1166
+ this.write('{}')
1167
+ return
1168
+ }
1169
+
1170
+ const hasComments = this.hasPendingCommentInRange(
1171
+ object.range.start.line,
1172
+ object.range.end.line
1173
+ )
1174
+ const inline = this.objectLiteralToString(object, sort)
1175
+ if (
1176
+ !hasComments &&
1177
+ inline.length + this.indentLevel * this.opts.indentSize <= 78 &&
1178
+ !inline.includes('\n')
1179
+ ) {
1180
+ this.write(inline)
1181
+ return
1182
+ }
1183
+
1184
+ const ordered = sort && !hasComments ? this.sortObjectEntries(entries) : entries
1185
+ this.write('{')
1186
+ this.newline()
1187
+ this.indentLevel++
1188
+ for (let i = 0; i < ordered.length; i++) {
1189
+ const entry = ordered[i]!
1190
+ this.emitCommentsBefore(entry.range.start.line)
1191
+ this.writeIndent()
1192
+ this.write(`${this.fieldName(entry.key, entry.isQuoted)}: ${this.expr(entry.value)}`)
1193
+ if (i < ordered.length - 1) this.write(',')
1194
+ this.newline()
1195
+ }
1196
+ this.emitCommentsBefore(object.range.end.line)
1197
+ this.indentLevel--
1198
+ this.writeIndent()
1199
+ this.write('}')
1200
+ }
1201
+
1202
+ private objectLiteralToString(object: ObjectLiteral, sort = false): string {
1203
+ if (object.entries.length === 0) return '{}'
1204
+ const entries = sort ? this.sortObjectEntries(object.entries) : object.entries
1205
+ const inner = entries
1206
+ .map((e) => `${this.fieldName(e.key, e.isQuoted)}: ${this.expr(e.value)}`)
1207
+ .join(', ')
1208
+ return `{${inner}}`
1209
+ }
1210
+
1211
+ private objectPatternToString(pattern: ObjectPattern): string {
1212
+ if (pattern.members.length === 0) return '{}'
1213
+ const inner = pattern.members
1214
+ .map((e) => `${this.fieldName(e.key, e.isQuoted)}: ${this.expr(e.value)}`)
1215
+ .join(', ')
1216
+ return `{${inner}}`
1217
+ }
1218
+
1219
+ private sortObjectEntries(entries: ObjectEntry[]): ObjectEntry[] {
1220
+ // Alphabetical by key (attributes are an unordered map in KIP).
1221
+ return [...entries].sort((a, b) => a.key.localeCompare(b.key))
1222
+ }
1223
+
1224
+ private fieldName(key: string, isQuoted: boolean): string {
1225
+ return isQuoted ? `"${this.escapeString(key)}"` : key
1226
+ }
1227
+
1228
+ // ────────────────────────────────────────────────────────────────────
1229
+ // Terms and expressions
1230
+ // ────────────────────────────────────────────────────────────────────
1231
+
1232
+ private tupleToString(tuple: PropositionTuple): string {
1233
+ if (tuple.id) return `(id: ${this.scalar(tuple.id)})`
1234
+ return `(${this.term(tuple.subject!)}, ${this.predicate(tuple.predicate!)}, ${this.term(tuple.object!)})`
1235
+ }
1236
+
1237
+ private term(term: Term): string {
1238
+ if (term.kind === 'ObjectPattern') return this.objectPatternToString(term)
1239
+ if (term.kind === 'PropositionTuple') return this.tupleToString(term)
1240
+ return this.expr(term)
1241
+ }
1242
+
1243
+ private predicate(expr: RawPredicateExpression): string {
1244
+ return expr.atoms
1245
+ .map((atom) => {
1246
+ const base = this.predAtom(atom.atom)
1247
+ if (!atom.quantifier) return base
1248
+ const q = atom.quantifier
1249
+ if (!q.hasComma) return `${base}{${q.min}}`
1250
+ return q.max === undefined
1251
+ ? `${base}{${q.min},}`
1252
+ : `${base}{${q.min},${q.max}}`
1253
+ })
1254
+ .join(' | ')
1255
+ }
1256
+
1257
+ private predAtom(atom: PredicateAtom): string {
1258
+ return this.expr(atom)
1259
+ }
1260
+
1261
+ private symbol(symbol: SchemaSymbol): string {
1262
+ return symbol.kind === 'ParameterRef' ? symbol.name : symbol.value
1263
+ }
1264
+
1265
+ private scalar(value: ScalarValue): string {
1266
+ return this.expr(value)
1267
+ }
1268
+
1269
+ private targetRef(ref: TargetRef): string {
1270
+ return this.expr(ref)
1271
+ }
1272
+
1273
+ private expr(expr: Expression): string {
1274
+ switch (expr.kind) {
1275
+ case 'StringLiteral':
1276
+ return expr.value
1277
+ case 'NumberLiteral':
1278
+ return expr.raw
1279
+ case 'BooleanLiteral':
1280
+ return expr.value ? 'true' : 'false'
1281
+ case 'NullLiteral':
1282
+ return 'null'
1283
+ case 'VariableRef':
1284
+ return expr.name
1285
+ case 'ParameterRef':
1286
+ return expr.name
1287
+ case 'FieldAccess':
1288
+ return (
1289
+ expr.base.name +
1290
+ expr.steps
1291
+ .map((step) =>
1292
+ step.kind === 'DotStep' ? `.${step.name}` : `[${step.key.value}]`
1293
+ )
1294
+ .join('')
1295
+ )
1296
+ case 'FunctionCallExpr':
1297
+ return `${expr.name.toUpperCase()}(${expr.args.map((a) => this.expr(a)).join(', ')})`
1298
+ case 'AggregateExpr':
1299
+ return `${expr.name}(${expr.distinct ? 'DISTINCT ' : ''}${this.expr(expr.argument)})`
1300
+ case 'BinaryExpression':
1301
+ return `${this.expr(expr.left)} ${expr.operator} ${this.expr(expr.right)}`
1302
+ case 'UnaryExpression':
1303
+ return `${expr.operator}${this.expr(expr.operand)}`
1304
+ case 'ArrayLiteral':
1305
+ return `[${expr.elements.map((e) => this.expr(e)).join(', ')}]`
1306
+ case 'ObjectLiteral':
1307
+ return this.objectLiteralToString(expr)
1308
+ case 'ObjectPattern':
1309
+ return this.objectPatternToString(expr)
1310
+ case 'PropositionTuple':
1311
+ return this.tupleToString(expr)
1312
+ }
1313
+ }
1314
+
1315
+ private escapeString(s: string): string {
1316
+ return s
1317
+ .replace(/\\/g, '\\\\')
1318
+ .replace(/"/g, '\\"')
1319
+ .replace(/\n/g, '\\n')
1320
+ .replace(/\t/g, '\\t')
1321
+ .replace(/\r/g, '\\r')
1322
+ }
1323
+
1324
+ private indent(): string {
1325
+ return ' '.repeat(this.indentLevel * this.opts.indentSize)
1326
+ }
1327
+
1328
+ private write(s: string): void {
1329
+ this.output += s
1330
+ }
1331
+
1332
+ private writeIndent(): void {
1333
+ this.output += this.indent()
1334
+ }
1335
+
1336
+ private newline(): void {
1337
+ this.output += '\n'
1338
+ }
1339
+ }