@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900

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 (147) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +226 -120
  3. package/dist/index.js +11552 -5580
  4. package/dist/index.js.map +1 -1
  5. package/package.json +12 -5
  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 +939 -1204
  13. package/src/completion-provider.ts +209 -191
  14. package/src/complex-types/quantity-value.ts +410 -0
  15. package/src/complex-types/temporal.ts +1776 -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 +506 -468
  23. package/src/lexer.ts +192 -211
  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 +99 -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 +744 -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 +132 -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/extension-function.ts +84 -0
  64. package/src/operations/first-function.ts +1 -1
  65. package/src/operations/floor-function.ts +1 -1
  66. package/src/operations/greater-operator.ts +7 -9
  67. package/src/operations/greater-or-equal-operator.ts +7 -9
  68. package/src/operations/highBoundary-function.ts +120 -0
  69. package/src/operations/hourOf-function.ts +66 -0
  70. package/src/operations/iif-function.ts +193 -8
  71. package/src/operations/implies-operator.ts +2 -1
  72. package/src/operations/in-operator.ts +2 -1
  73. package/src/operations/index.ts +43 -0
  74. package/src/operations/indexOf-function.ts +1 -1
  75. package/src/operations/intersect-function.ts +1 -1
  76. package/src/operations/is-function.ts +70 -0
  77. package/src/operations/is-operator.ts +176 -13
  78. package/src/operations/isDistinct-function.ts +2 -1
  79. package/src/operations/join-function.ts +1 -1
  80. package/src/operations/last-function.ts +1 -1
  81. package/src/operations/lastIndexOf-function.ts +85 -0
  82. package/src/operations/length-function.ts +1 -1
  83. package/src/operations/less-operator.ts +8 -9
  84. package/src/operations/less-or-equal-operator.ts +7 -9
  85. package/src/operations/less-than.ts +8 -13
  86. package/src/operations/lowBoundary-function.ts +120 -0
  87. package/src/operations/lower-function.ts +1 -1
  88. package/src/operations/matches-function.ts +86 -0
  89. package/src/operations/matchesFull-function.ts +96 -0
  90. package/src/operations/millisecondOf-function.ts +66 -0
  91. package/src/operations/minus-operator.ts +76 -4
  92. package/src/operations/minuteOf-function.ts +66 -0
  93. package/src/operations/mod-operator.ts +8 -2
  94. package/src/operations/monthOf-function.ts +66 -0
  95. package/src/operations/multiply-operator.ts +27 -3
  96. package/src/operations/not-equal-operator.ts +24 -30
  97. package/src/operations/not-equivalent-operator.ts +13 -53
  98. package/src/operations/not-function.ts +10 -3
  99. package/src/operations/ofType-function.ts +43 -12
  100. package/src/operations/or-operator.ts +2 -1
  101. package/src/operations/plus-operator.ts +71 -7
  102. package/src/operations/power-function.ts +35 -10
  103. package/src/operations/precision-function.ts +146 -0
  104. package/src/operations/repeat-function.ts +169 -0
  105. package/src/operations/replace-function.ts +1 -1
  106. package/src/operations/replaceMatches-function.ts +125 -0
  107. package/src/operations/round-function.ts +1 -1
  108. package/src/operations/secondOf-function.ts +66 -0
  109. package/src/operations/select-function.ts +66 -5
  110. package/src/operations/single-function.ts +1 -1
  111. package/src/operations/skip-function.ts +1 -1
  112. package/src/operations/split-function.ts +1 -1
  113. package/src/operations/sqrt-function.ts +15 -8
  114. package/src/operations/startsWith-function.ts +1 -1
  115. package/src/operations/subsetOf-function.ts +6 -2
  116. package/src/operations/substring-function.ts +1 -1
  117. package/src/operations/supersetOf-function.ts +6 -2
  118. package/src/operations/tail-function.ts +1 -1
  119. package/src/operations/take-function.ts +1 -1
  120. package/src/operations/temporal-functions.ts +555 -0
  121. package/src/operations/timeOf-function.ts +67 -0
  122. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  123. package/src/operations/toBoolean-function.ts +27 -8
  124. package/src/operations/toChars-function.ts +56 -0
  125. package/src/operations/toDecimal-function.ts +27 -8
  126. package/src/operations/toInteger-function.ts +15 -3
  127. package/src/operations/toLong-function.ts +98 -0
  128. package/src/operations/toQuantity-function.ts +181 -0
  129. package/src/operations/toString-function.ts +78 -15
  130. package/src/operations/trace-function.ts +1 -1
  131. package/src/operations/trim-function.ts +1 -1
  132. package/src/operations/truncate-function.ts +1 -1
  133. package/src/operations/unary-minus-operator.ts +2 -2
  134. package/src/operations/unary-plus-operator.ts +1 -1
  135. package/src/operations/union-function.ts +1 -1
  136. package/src/operations/union-operator.ts +16 -26
  137. package/src/operations/upper-function.ts +1 -1
  138. package/src/operations/where-function.ts +3 -3
  139. package/src/operations/xor-operator.ts +1 -1
  140. package/src/operations/yearOf-function.ts +66 -0
  141. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  142. package/src/parser.ts +262 -503
  143. package/src/registry.ts +53 -42
  144. package/src/types.ts +129 -17
  145. package/src/utils/decimal.ts +76 -0
  146. package/src/utils/pprint.ts +151 -0
  147. package/src/quantity-value.ts +0 -198
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,15 +227,17 @@ 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
- valueType: 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
233
+ valueType: 'string' | 'number' | 'decimal' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
234
+ }
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 @)
223
241
  }
224
242
 
225
243
  export interface BinaryNode extends BaseASTNode {
@@ -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,76 @@
1
+ /**
2
+ * Utility functions for handling decimal precision in FHIRPath
3
+ */
4
+
5
+ /**
6
+ * Get the number of decimal places in a number
7
+ */
8
+ export function getDecimalPlaces(n: number): number {
9
+ if (Number.isInteger(n)) {
10
+ return 0;
11
+ }
12
+
13
+ // Convert to string and count decimal places
14
+ // Use toFixed(10) then strip trailing zeros to get actual precision
15
+ const str = n.toFixed(10).replace(/0+$/, '').replace(/\.$/, '');
16
+ const decimalIndex = str.indexOf('.');
17
+
18
+ if (decimalIndex === -1) {
19
+ return 0;
20
+ }
21
+
22
+ return str.length - decimalIndex - 1;
23
+ }
24
+
25
+ /**
26
+ * Round a number to a specific number of decimal places
27
+ */
28
+ export function roundToDecimalPlaces(n: number, places: number): number {
29
+ if (places < 0) {
30
+ throw new Error('Decimal places must be non-negative');
31
+ }
32
+
33
+ const multiplier = Math.pow(10, places);
34
+ return Math.round(n * multiplier) / multiplier;
35
+ }
36
+
37
+ /**
38
+ * Normalize a decimal result based on the precision of the operands
39
+ * According to FHIRPath spec, arithmetic operations should preserve
40
+ * the appropriate precision based on the input values
41
+ */
42
+ export function normalizeDecimalResult(result: number, leftOperand: number, rightOperand: number): number {
43
+ // If result is effectively an integer, return it as is
44
+ if (Number.isInteger(result)) {
45
+ return result;
46
+ }
47
+
48
+ // Get the precision of both operands
49
+ const leftPlaces = getDecimalPlaces(leftOperand);
50
+ const rightPlaces = getDecimalPlaces(rightOperand);
51
+
52
+ // For addition and subtraction, use the maximum precision
53
+ // For multiplication, sum the precisions
54
+ // For division and modulo, use a reasonable precision (8 decimal places max)
55
+ const maxPrecision = Math.max(leftPlaces, rightPlaces);
56
+
57
+ // Round to the appropriate precision to eliminate floating point artifacts
58
+ return roundToDecimalPlaces(result, Math.min(maxPrecision, 8));
59
+ }
60
+
61
+ /**
62
+ * Normalize a decimal for modulo operation
63
+ * Modulo has special precision rules
64
+ */
65
+ export function normalizeModuloResult(result: number, leftOperand: number, rightOperand: number): number {
66
+ // If result is effectively an integer, return it as is
67
+ if (Number.isInteger(result)) {
68
+ return result;
69
+ }
70
+
71
+ // For modulo, use the precision of the divisor (right operand)
72
+ const rightPlaces = getDecimalPlaces(rightOperand);
73
+
74
+ // Round to the appropriate precision
75
+ return roundToDecimalPlaces(result, Math.min(rightPlaces, 8));
76
+ }
@@ -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
+ }