@atomic-ehr/fhirpath 0.0.2 → 0.0.3

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 (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
package/src/registry.ts CHANGED
@@ -161,7 +161,7 @@ export class Registry {
161
161
  /**
162
162
  * Get functions applicable to a specific type
163
163
  */
164
- getFunctionsForType(typeName: TypeName | string): FunctionDefinition[] {
164
+ getFunctionsForType = (typeName: TypeName | string): FunctionDefinition[] => {
165
165
  const results: FunctionDefinition[] = [];
166
166
 
167
167
  for (const [_, func] of this.functions) {
@@ -200,53 +200,64 @@ export class Registry {
200
200
  /**
201
201
  * Check if a function is applicable to a type
202
202
  */
203
- isFunctionApplicableToType(functionName: string, typeName: TypeName | string): boolean {
203
+ private getTypeInfoFromString(type: string): TypeInfo {
204
+ const isCollection = type.endsWith('[]');
205
+ const typeName = isCollection ? type.slice(0, -2) : type;
206
+ return {
207
+ type: typeName as TypeName,
208
+ singleton: !isCollection,
209
+ };
210
+ }
211
+
212
+ public isTypeCompatible(inputType: TypeInfo, requiredType: TypeInfo): boolean {
213
+ // Check singleton compatibility
214
+ if (requiredType.singleton && !inputType.singleton) {
215
+ return false;
216
+ }
217
+
218
+ const numericTypes: TypeName[] = ['Integer', 'Decimal'];
219
+ const temporalTypes: TypeName[] = ['Date', 'DateTime', 'Time'];
220
+
221
+ // 'Any' type is always compatible
222
+ if (requiredType.type === 'Any') {
223
+ return true;
224
+ }
225
+
226
+ // Direct type match
227
+ if (inputType.type === requiredType.type) {
228
+ return true;
229
+ }
230
+
231
+ // Numeric types are compatible with each other
232
+ if (numericTypes.includes(inputType.type) && numericTypes.includes(requiredType.type)) {
233
+ return true;
234
+ }
235
+
236
+ // Temporal types are compatible with each other
237
+ if (temporalTypes.includes(inputType.type) && temporalTypes.includes(requiredType.type)) {
238
+ return true;
239
+ }
240
+
241
+ // TODO: Add model-aware subtype checking here
242
+ // For now, we only handle the above cases.
243
+ // A full implementation would require a ModelProvider to check subtype relationships.
244
+
245
+ return false;
246
+ }
247
+
248
+ isFunctionApplicableToType(functionName: string, type: string): boolean {
204
249
  const func = this.getFunction(functionName);
205
250
  if (!func) return false;
206
-
207
- // If no signatures, function works with any type
208
- if (!func.signatures || func.signatures.length === 0) return true;
209
-
210
- // Check if we're dealing with a collection type
211
- const isCollection = typeof typeName === 'string' && typeName.endsWith('[]');
212
-
213
- // Check if ANY signature matches the type
251
+
252
+ // New logic: check all signatures
214
253
  for (const signature of func.signatures) {
215
- // If no input type specified, this signature works with any type
216
- if (!signature.input) return true;
217
-
218
- const inputType = signature.input.type;
219
- const requiresSingleton = signature.input.singleton;
220
-
221
- // If function requires singleton but we have a collection, skip this signature
222
- if (requiresSingleton && isCollection) {
223
- continue;
224
- }
225
-
226
- // 'Any' type accepts all inputs (but still respects singleton constraint checked above)
227
- if (inputType === 'Any') return true;
228
-
229
- // Direct type match
230
- if (inputType === typeName) return true;
231
-
232
- // For collection types, check if function can work with collections
233
- if (typeof typeName === 'string' && typeName.endsWith('[]')) {
234
- const itemType = typeName.slice(0, -2);
235
- // Only allow if function doesn't require singleton
236
- if (inputType === itemType && !requiresSingleton) {
237
- return true;
238
- }
239
- }
240
-
241
- // Check if it's a numeric type and function accepts numeric types
242
- const numericTypes = ['Integer', 'Decimal', 'Number'];
243
- if (numericTypes.includes(typeName as string) && numericTypes.includes(inputType as string)) {
254
+ if (!signature.input) {
255
+ // No input constraint, so it's applicable
244
256
  return true;
245
257
  }
246
258
 
247
- // Check if it's a temporal type and function accepts temporal types
248
- const temporalTypes = ['Date', 'DateTime', 'Time', 'Instant'];
249
- if (temporalTypes.includes(typeName as string) && temporalTypes.includes(inputType as string)) {
259
+ const inputType = this.getTypeInfoFromString(type);
260
+ if (this.isTypeCompatible(inputType, signature.input)) {
250
261
  return true;
251
262
  }
252
263
  }
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Token, TokenType } from './lexer';
2
+ import type { AnyCursorNode } from './parser/cursor-nodes';
2
3
 
3
4
  // Precedence levels (higher number = higher precedence)
4
5
  export enum PRECEDENCE {
@@ -25,6 +26,9 @@ export interface TypeInfo<TypeContext = unknown> {
25
26
  // FHIRPath type
26
27
  type: TypeName;
27
28
  singleton?: boolean;
29
+
30
+ // Indicates this is definitely an empty collection
31
+ isEmpty?: boolean;
28
32
 
29
33
  // Model type information FHIR.Patient; FHIR.string;
30
34
  namespace?: string;
@@ -77,6 +81,7 @@ export interface OperatorSignature {
77
81
  export interface OperatorDefinition {
78
82
  symbol: string;
79
83
  name: string;
84
+ doesNotPropagateEmpty?: boolean;
80
85
  category: string[];
81
86
  precedence: PRECEDENCE;
82
87
  associativity: 'left' | 'right';
@@ -97,6 +102,7 @@ export interface FunctionSignature {
97
102
  optional?: boolean;
98
103
  type: TypeInfo;
99
104
  expression?: boolean;
105
+ typeReference?: boolean; // When true, this parameter expects a type name (e.g., ofType(Patient))
100
106
  }>;
101
107
  result: TypeInfo | 'inputType' | 'inputTypeSingleton' | 'parameterType';
102
108
  }
@@ -108,6 +114,16 @@ export interface FunctionDefinition {
108
114
  examples: string[];
109
115
  signatures: FunctionSignature[];
110
116
  evaluate: FunctionEvaluator;
117
+ inferResultType?: (
118
+ analyzer: any,
119
+ node: any,
120
+ inputType?: TypeInfo
121
+ ) => Promise<TypeInfo>;
122
+ analyze?: (
123
+ context: AnalysisContext,
124
+ args: ASTNode[]
125
+ ) => Promise<InternalAnalysisResult> | InternalAnalysisResult;
126
+ doesNotPropagateEmpty?: boolean; // When true, function doesn't propagate empty collections
111
127
  }
112
128
 
113
129
  // Node types enum - string-based for better debugging
@@ -115,9 +131,9 @@ export enum NodeType {
115
131
  EOF = 'EOF',
116
132
  Binary = 'Binary',
117
133
  Unary = 'Unary',
118
- TypeOrIdentifier = 'TypeOrIdentifier',
119
134
  Identifier = 'Identifier',
120
135
  Literal = 'Literal',
136
+ TemporalLiteral = 'TemporalLiteral',
121
137
  Function = 'Function',
122
138
  Variable = 'Variable',
123
139
  Index = 'Index',
@@ -170,7 +186,7 @@ export interface ParserOptions {
170
186
  // Base structure for all AST nodes
171
187
  export interface BaseASTNode {
172
188
  // Core properties - always present
173
- type: NodeType | 'Error';
189
+ type: NodeType | 'Error' | 'CursorNode';
174
190
 
175
191
  // LSP-compatible range - always present for LSP features
176
192
  range: Range;
@@ -211,17 +227,19 @@ export interface IdentifierNode extends BaseASTNode {
211
227
  symbolKind?: SymbolKind.Variable | SymbolKind.Function | SymbolKind.Property;
212
228
  }
213
229
 
214
- export interface TypeOrIdentifierNode extends BaseASTNode {
215
- type: NodeType.TypeOrIdentifier;
216
- name: string;
217
- }
218
-
219
230
  export interface LiteralNode extends BaseASTNode {
220
231
  type: NodeType.Literal;
221
232
  value: any;
222
233
  valueType: 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
223
234
  }
224
235
 
236
+ export interface TemporalLiteralNode extends BaseASTNode {
237
+ type: NodeType.TemporalLiteral;
238
+ value: any; // The parsed temporal object
239
+ valueType: 'date' | 'time' | 'datetime';
240
+ rawValue: string; // The original string value (without @)
241
+ }
242
+
225
243
  export interface BinaryNode extends BaseASTNode {
226
244
  type: NodeType.Binary;
227
245
  operator: string;
@@ -281,14 +299,13 @@ export interface QuantityNode extends BaseASTNode {
281
299
  type: NodeType.Quantity;
282
300
  value: number;
283
301
  unit: string;
284
- isCalendarUnit?: boolean;
285
302
  }
286
303
 
287
304
  // Unified ASTNode type - discriminated union
288
305
  export type ASTNode =
289
306
  | IdentifierNode
290
- | TypeOrIdentifierNode
291
307
  | LiteralNode
308
+ | TemporalLiteralNode
292
309
  | BinaryNode
293
310
  | UnaryNode
294
311
  | FunctionNode
@@ -299,7 +316,8 @@ export type ASTNode =
299
316
  | CollectionNode
300
317
  | TypeReferenceNode
301
318
  | QuantityNode
302
- | ErrorNode;
319
+ | ErrorNode
320
+ | AnyCursorNode;
303
321
 
304
322
  export interface RuntimeContext {
305
323
  input: any[];
@@ -311,7 +329,7 @@ export interface RuntimeContext {
311
329
 
312
330
  // Evaluation result - everything is a collection of boxed values
313
331
  export interface EvaluationResult {
314
- value: import('./boxing').FHIRPathValue[];
332
+ value: import('./interpreter/boxing').FHIRPathValue[];
315
333
  context: RuntimeContext;
316
334
  }
317
335
 
@@ -337,6 +355,8 @@ export interface Diagnostic {
337
355
  export interface AnalysisResult {
338
356
  diagnostics: Diagnostic[];
339
357
  ast: ASTNode;
358
+ type?: TypeInfo;
359
+ userVariables?: Map<string, TypeInfo>;
340
360
  }
341
361
 
342
362
  // Parse error type
@@ -353,7 +373,7 @@ export interface ParseResult {
353
373
  errors: ParseError[];
354
374
  indexes?: {
355
375
  nodeById: Map<string, ASTNode>;
356
- nodesByType: Map<NodeType | 'Error', ASTNode[]>;
376
+ nodesByType: Map<NodeType | 'Error' | 'CursorNode', ASTNode[]>;
357
377
  identifiers: Map<string, ASTNode[]>;
358
378
  };
359
379
  cursorContext?: {
@@ -363,15 +383,15 @@ export interface ParseResult {
363
383
  };
364
384
  }
365
385
 
366
- export type NodeEvaluator = (node: ASTNode, input: import('./boxing').FHIRPathValue[], context: RuntimeContext) => Promise<EvaluationResult>;
386
+ export type NodeEvaluator = (node: ASTNode, input: import('./interpreter/boxing').FHIRPathValue[], context: RuntimeContext) => Promise<EvaluationResult>;
367
387
 
368
- export type OperationEvaluator = (input: import('./boxing').FHIRPathValue[], context: RuntimeContext, ...args: any[]) => Promise<EvaluationResult>;
388
+ export type OperationEvaluator = (input: import('./interpreter/boxing').FHIRPathValue[], context: RuntimeContext, ...args: any[]) => Promise<EvaluationResult>;
369
389
 
370
390
  export type FunctionEvaluator = (
371
- input: import('./boxing').FHIRPathValue[],
391
+ input: import('./interpreter/boxing').FHIRPathValue[],
372
392
  context: RuntimeContext,
373
393
  args: ASTNode[],
374
- evaluator: (node: ASTNode, input: import('./boxing').FHIRPathValue[], context: RuntimeContext) => Promise<EvaluationResult>
394
+ evaluator: (node: ASTNode, input: import('./interpreter/boxing').FHIRPathValue[], context: RuntimeContext) => Promise<EvaluationResult>
375
395
  ) => Promise<EvaluationResult>;
376
396
 
377
397
  // Type guards for optional properties
@@ -403,3 +423,95 @@ export function isFunctionNode(node: ASTNode): node is FunctionNode {
403
423
  return node.type === NodeType.Function;
404
424
  }
405
425
 
426
+ /**
427
+ * Result of analyzing a single AST node in the context-flow architecture.
428
+ */
429
+ export interface InternalAnalysisResult {
430
+ type: TypeInfo;
431
+ diagnostics: Diagnostic[];
432
+ context?: AnalysisContext;
433
+ }
434
+
435
+ /**
436
+ * Immutable context that flows through the analysis tree.
437
+ * Carries variable scopes and input types through the AST.
438
+ */
439
+ export class AnalysisContext {
440
+ constructor(
441
+ public readonly inputType: TypeInfo,
442
+ public readonly systemVariables: ReadonlyMap<string, TypeInfo>,
443
+ public readonly userVariables: ReadonlyMap<string, TypeInfo>,
444
+ private readonly analyzeNodeCallback: (node: ASTNode, ctx: AnalysisContext) => Promise<InternalAnalysisResult>,
445
+ public readonly modelProvider?: ModelProvider,
446
+ public readonly hasDynamicVariables: boolean = false,
447
+ public readonly _chainHead: boolean = true
448
+ ) {}
449
+
450
+ withUserVariable(name: string, type: TypeInfo): AnalysisContext {
451
+ const newUserVars = new Map(this.userVariables);
452
+ newUserVars.set(name, type);
453
+ return new AnalysisContext(
454
+ this.inputType,
455
+ this.systemVariables,
456
+ newUserVars,
457
+ this.analyzeNodeCallback,
458
+ this.modelProvider,
459
+ this.hasDynamicVariables,
460
+ this._chainHead
461
+ );
462
+ }
463
+
464
+ withSystemVariable(name: string, type: TypeInfo): AnalysisContext {
465
+ const newSystemVars = new Map(this.systemVariables);
466
+ newSystemVars.set(name, type);
467
+ return new AnalysisContext(
468
+ this.inputType,
469
+ newSystemVars,
470
+ this.userVariables,
471
+ this.analyzeNodeCallback,
472
+ this.modelProvider,
473
+ this.hasDynamicVariables,
474
+ this._chainHead
475
+ );
476
+ }
477
+
478
+ withInputType(type: TypeInfo): AnalysisContext {
479
+ return new AnalysisContext(
480
+ type,
481
+ this.systemVariables,
482
+ this.userVariables,
483
+ this.analyzeNodeCallback,
484
+ this.modelProvider,
485
+ this.hasDynamicVariables,
486
+ false
487
+ );
488
+ }
489
+
490
+ withDynamicVariables(): AnalysisContext {
491
+ return new AnalysisContext(
492
+ this.inputType,
493
+ this.systemVariables,
494
+ this.userVariables,
495
+ this.analyzeNodeCallback,
496
+ this.modelProvider,
497
+ true,
498
+ this._chainHead
499
+ );
500
+ }
501
+
502
+ fork(): AnalysisContext {
503
+ return new AnalysisContext(
504
+ this.inputType,
505
+ new Map(this.systemVariables),
506
+ new Map(this.userVariables),
507
+ this.analyzeNodeCallback,
508
+ this.modelProvider,
509
+ this.hasDynamicVariables,
510
+ this._chainHead
511
+ );
512
+ }
513
+
514
+ analyzeNode(node: ASTNode): Promise<InternalAnalysisResult> {
515
+ return this.analyzeNodeCallback(node, this);
516
+ }
517
+ }
@@ -0,0 +1,151 @@
1
+ import { NodeType } from '../types';
2
+ import type {
3
+ ASTNode,
4
+ IdentifierNode,
5
+ LiteralNode,
6
+ UnaryNode,
7
+ BinaryNode,
8
+ FunctionNode,
9
+ IndexNode,
10
+ MembershipTestNode,
11
+ TypeCastNode,
12
+ CollectionNode,
13
+ TypeReferenceNode,
14
+ QuantityNode,
15
+ } from '../types';
16
+
17
+ export function pprint(node: ASTNode, indent: number = 0): string {
18
+ const spaces = ' '.repeat(indent);
19
+
20
+ switch (node.type) {
21
+ case NodeType.Literal: {
22
+ const lit = node as LiteralNode;
23
+ if (lit.valueType === 'string') {
24
+ return `"${lit.value}"`;
25
+ } else if (lit.valueType === 'null') {
26
+ return 'null';
27
+ }
28
+ return String(lit.value);
29
+ }
30
+
31
+ case NodeType.Identifier: {
32
+ const id = node as IdentifierNode;
33
+ return id.name;
34
+ }
35
+
36
+ case NodeType.Variable: {
37
+ return (node as any).name;
38
+ }
39
+
40
+ case NodeType.Binary: {
41
+ const bin = node as BinaryNode;
42
+ const op = bin.operator;
43
+
44
+ const leftStr = pprint(bin.left, 0);
45
+ const rightStr = pprint(bin.right, 0);
46
+
47
+ if (leftStr.length + rightStr.length + op.length + 4 < 60 &&
48
+ !leftStr.includes('\n') && !rightStr.includes('\n')) {
49
+ return `(${op} ${leftStr} ${rightStr})`;
50
+ }
51
+
52
+ return `(${op}\n${spaces} ${pprint(bin.left, indent + 2)}\n${spaces} ${pprint(bin.right, indent + 2)})`;
53
+ }
54
+
55
+ case NodeType.Unary: {
56
+ const un = node as UnaryNode;
57
+ const operandStr = pprint(un.operand, 0);
58
+
59
+ if (operandStr.length < 40 && !operandStr.includes('\n')) {
60
+ return `(${un.operator} ${operandStr})`;
61
+ }
62
+
63
+ return `(${un.operator}\n${spaces} ${pprint(un.operand, indent + 2)})`;
64
+ }
65
+
66
+ case NodeType.Function: {
67
+ const fn = node as FunctionNode;
68
+ const nameStr = pprint(fn.name, 0);
69
+
70
+ if (fn.arguments.length === 0) {
71
+ return `(${nameStr})`;
72
+ }
73
+
74
+ const argStrs = fn.arguments.map(arg => pprint(arg, 0));
75
+ const totalLen = nameStr.length + argStrs.reduce((sum, s) => sum + s.length + 1, 0) + 2;
76
+
77
+ if (totalLen < 60 && argStrs.every(s => !s.includes('\n'))) {
78
+ return `(${nameStr} ${argStrs.join(' ')})`;
79
+ }
80
+
81
+ const argLines = fn.arguments.map(arg => `${spaces} ${pprint(arg, indent + 2)}`);
82
+ return `(${nameStr}\n${argLines.join('\n')})`;
83
+ }
84
+
85
+ case NodeType.Index: {
86
+ const idx = node as IndexNode;
87
+ const exprStr = pprint(idx.expression, 0);
88
+ const indexStr = pprint(idx.index, 0);
89
+
90
+ if (exprStr.length + indexStr.length < 50 &&
91
+ !exprStr.includes('\n') && !indexStr.includes('\n')) {
92
+ return `([] ${exprStr} ${indexStr})`;
93
+ }
94
+
95
+ return `([]\n${spaces} ${pprint(idx.expression, indent + 2)}\n${spaces} ${pprint(idx.index, indent + 2)})`;
96
+ }
97
+
98
+ case NodeType.MembershipTest: {
99
+ const mt = node as MembershipTestNode;
100
+ const exprStr = pprint(mt.expression, 0);
101
+
102
+ if (exprStr.length + mt.targetType.length < 50 && !exprStr.includes('\n')) {
103
+ return `(is ${exprStr} ${mt.targetType})`;
104
+ }
105
+
106
+ return `(is\n${spaces} ${pprint(mt.expression, indent + 2)}\n${spaces} ${mt.targetType})`;
107
+ }
108
+
109
+ case NodeType.TypeCast: {
110
+ const tc = node as TypeCastNode;
111
+ const exprStr = pprint(tc.expression, 0);
112
+
113
+ if (exprStr.length + tc.targetType.length < 50 && !exprStr.includes('\n')) {
114
+ return `(as ${exprStr} ${tc.targetType})`;
115
+ }
116
+
117
+ return `(as\n${spaces} ${pprint(tc.expression, indent + 2)}\n${spaces} ${tc.targetType})`;
118
+ }
119
+
120
+ case NodeType.Collection: {
121
+ const coll = node as CollectionNode;
122
+
123
+ if (coll.elements.length === 0) {
124
+ return '{}';
125
+ }
126
+
127
+ const elemStrs = coll.elements.map(e => pprint(e, 0));
128
+ const totalLen = elemStrs.reduce((sum, s) => sum + s.length + 1, 2);
129
+
130
+ if (totalLen < 60 && elemStrs.every(s => !s.includes('\n'))) {
131
+ return `{${elemStrs.join(' ')}}`;
132
+ }
133
+
134
+ const elemLines = coll.elements.map(e => `${spaces} ${pprint(e, indent + 2)}`);
135
+ return `{\n${elemLines.join('\n')}\n${spaces}}`;
136
+ }
137
+
138
+ case NodeType.TypeReference: {
139
+ const tr = node as TypeReferenceNode;
140
+ return `Type[${tr.typeName}]`;
141
+ }
142
+
143
+ case NodeType.Quantity: {
144
+ const q = node as QuantityNode;
145
+ return `${q.value} '${q.unit}'`;
146
+ }
147
+
148
+ default:
149
+ return `<unknown:${(node as any).type}>`;
150
+ }
151
+ }