@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
@@ -172,7 +172,8 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
172
172
 
173
173
  return schema;
174
174
  } catch (error) {
175
- console.warn(`Failed to load schema for ${typeName}:`, error);
175
+ // Silently fail for unknown types - this is expected for non-FHIR types
176
+ // like function names or property names that get speculatively checked
176
177
  return undefined;
177
178
  }
178
179
  }
@@ -189,7 +190,8 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
189
190
  let current = schema;
190
191
 
191
192
  // Walk up the inheritance chain
192
- while (current.base && current.base !== 'Resource' && current.base !== 'Element') {
193
+ // Include all base types including DomainResource, Resource, and Element
194
+ while (current.base) {
193
195
  // Extract just the type name from the base URL if it's a full URL
194
196
  let baseTypeName = current.base;
195
197
  if (baseTypeName && baseTypeName.startsWith('http://')) {
@@ -277,10 +279,54 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
277
279
 
278
280
  // Async implementation with lazy loading
279
281
  async getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined> {
280
- // Check if it's a primitive type - these don't require initialization
281
- if (this.typeMapping[typeName]) {
282
+ // First check if this is a FHIRPath primitive type name
283
+ const fhirpathPrimitives = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Any'];
284
+ if (fhirpathPrimitives.includes(typeName)) {
285
+ // Return the FHIRPath primitive type directly without trying to load a schema
282
286
  return {
283
- type: this.typeMapping[typeName],
287
+ type: typeName as TypeName,
288
+ namespace: 'FHIR',
289
+ name: typeName,
290
+ singleton: true,
291
+ modelContext: {
292
+ path: typeName,
293
+ schemaHierarchy: []
294
+ }
295
+ };
296
+ }
297
+
298
+ // Check if it's a FHIR primitive type that maps to a FHIRPath type
299
+ const mappedType = this.typeMapping[typeName];
300
+ if (mappedType) {
301
+ // For types that are both FHIRPath types and complex FHIR types (like Quantity),
302
+ // we need to check if they have a schema
303
+ // But skip primitive FHIRPath types that definitely don't have schemas
304
+ const primitiveTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
305
+ const isPrimitive = primitiveTypes.includes(mappedType);
306
+
307
+ if (this.initialized && !isPrimitive) {
308
+ const schema = await this.getSchema(typeName);
309
+ if (schema) {
310
+ // It's a complex type with properties
311
+ const schemaHierarchy = await this.getSchemaHierarchyAsync(schema);
312
+ return {
313
+ type: mappedType,
314
+ namespace: 'FHIR',
315
+ name: typeName,
316
+ singleton: true,
317
+ modelContext: {
318
+ path: typeName,
319
+ schemaHierarchy,
320
+ canonicalUrl: schema.url,
321
+ version: schema.version
322
+ }
323
+ };
324
+ }
325
+ }
326
+
327
+ // It's a pure primitive type without properties
328
+ return {
329
+ type: mappedType,
284
330
  namespace: 'FHIR',
285
331
  name: typeName,
286
332
  singleton: true,
@@ -324,6 +370,26 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
324
370
  const context = parentType.modelContext;
325
371
  if (!context) return undefined;
326
372
 
373
+ // Handle union types (polymorphic fields like value[x])
374
+ if (context.isUnion && context.choices) {
375
+ // For union types, check if the property exists on any of the choice types
376
+ for (const choice of context.choices) {
377
+ // Get the type for this choice - use type field which has the proper case
378
+ const choiceTypeName = choice.type;
379
+ const choiceType = await this.getType(choiceTypeName);
380
+ if (choiceType) {
381
+ // Try to get the property from this choice type
382
+ const elementType = await this.getElementType(choiceType, propertyName);
383
+ if (elementType) {
384
+ // Found the property on this choice type
385
+ return elementType;
386
+ }
387
+ }
388
+ }
389
+ // Property not found on any choice type
390
+ return undefined;
391
+ }
392
+
327
393
  // Search through schema hierarchy for the property
328
394
  for (const schema of context.schemaHierarchy) {
329
395
  const element = schema.elements?.[propertyName];
@@ -609,42 +675,4 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
609
675
  return this.primitiveTypesCache || [];
610
676
  }
611
677
 
612
- // Synchronous method to get type from cache (for analyzer)
613
- getTypeFromCache(typeName: string): TypeInfo<FHIRModelContext> | undefined {
614
- // Check if it's a primitive type - these don't require initialization
615
- if (this.typeMapping[typeName]) {
616
- return {
617
- type: this.typeMapping[typeName],
618
- namespace: 'FHIR',
619
- name: typeName,
620
- singleton: true,
621
- modelContext: {
622
- path: typeName,
623
- schemaHierarchy: []
624
- }
625
- };
626
- }
627
-
628
- // For complex types, check if schema is in cache
629
- const schema = this.schemaCache.get(typeName);
630
- if (!schema) {
631
- return undefined;
632
- }
633
-
634
- // Get cached hierarchy or at least the current schema
635
- const schemaHierarchy = this.hierarchyCache.get(schema.name || schema.url) || [schema];
636
-
637
- return {
638
- type: 'Any', // Complex types are 'Any' in FHIRPath
639
- namespace: 'FHIR',
640
- name: typeName,
641
- singleton: true,
642
- modelContext: {
643
- path: typeName,
644
- schemaHierarchy,
645
- canonicalUrl: schema.url,
646
- version: schema.version
647
- }
648
- };
649
- }
650
678
  }
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // abs() takes no arguments
@@ -1,7 +1,7 @@
1
- import type { FunctionDefinition, FunctionEvaluator } from '../types';
1
+ import type { FunctionDefinition, FunctionEvaluator, TypeInfo } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { RuntimeContextManager } from '../interpreter';
4
- import { box, unbox } from '../boxing';
3
+ import { RuntimeContextManager } from '../interpreter/runtime-context';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
7
  // aggregator expression is required
@@ -21,6 +21,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
21
21
  total = [];
22
22
  }
23
23
 
24
+ // If input is empty and init is provided, return the init value
25
+ if (input.length === 0 && initExpr) {
26
+ return { value: total, context };
27
+ }
28
+
24
29
  // For each item in the input collection, evaluate the aggregator expression
25
30
  for (let index = 0; index < input.length; index++) {
26
31
  const item = input[index]!;
@@ -46,6 +51,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
46
51
 
47
52
  export const aggregateFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
48
53
  name: 'aggregate',
54
+ doesNotPropagateEmpty: true, // aggregate with init should return init value for empty input
49
55
  category: ['aggregates'],
50
56
  description: 'Performs general-purpose aggregation by evaluating the aggregator expression for each element of the input collection',
51
57
  examples: [
@@ -63,5 +69,78 @@ export const aggregateFunction: FunctionDefinition & { evaluate: FunctionEvaluat
63
69
  ],
64
70
  result: { type: 'Any', singleton: false }
65
71
  }],
66
- evaluate
67
- };
72
+ evaluate,
73
+ async inferResultType(analyzer, node, inputType) {
74
+ // If init parameter is provided, use its type to infer result type
75
+ if (node.arguments.length >= 2) {
76
+ const initType = await (analyzer as any).inferType(node.arguments[1]!, inputType);
77
+ // The result type is the same as init type
78
+ return initType;
79
+ }
80
+ // Without init, we can't fully infer the type without running annotation
81
+ // This is a limitation - the actual type will be set during annotateAST
82
+ if (node.arguments.length >= 1) {
83
+ // We could try to infer, but it would require setting up system variables
84
+ // For now, return Any and let annotateAST handle proper typing
85
+ return { type: 'Any', singleton: false };
86
+ }
87
+ // No arguments at all
88
+ return { type: 'Any', singleton: false };
89
+ },
90
+ async analyze(context, args) {
91
+ const diagnostics: any[] = [];
92
+ const itemType = { ...context.inputType, singleton: true };
93
+
94
+ // Determine $total type: from init (arg[1]) if provided; otherwise from aggregator result after first iteration (approximate with Any)
95
+ let totalType = { type: 'Any', singleton: false } as TypeInfo;
96
+
97
+ if (args.length >= 2 && args[1]) {
98
+ const initResult = await context
99
+ .withSystemVariable('$this', itemType)
100
+ .withSystemVariable('$index', { type: 'Integer', singleton: true })
101
+ .analyzeNode(args[1]!);
102
+ diagnostics.push(...initResult.diagnostics);
103
+ totalType = initResult.type;
104
+ }
105
+
106
+ // Analyze aggregator with $this (item) and $total (init or inferred seed)
107
+ if (args.length >= 1 && args[0]) {
108
+ // If we don't have init, seed $total with a heuristic:
109
+ // - If aggregator contains string operations, seed as String
110
+ // - Else seed as item type
111
+ const containsStringHints = (function hasStringHints(node: any): boolean {
112
+ if (!node) return false;
113
+ if (node.type === 'Literal' && typeof node.value === 'string') return true;
114
+ if (node.type === 'Function' && node.name?.type === 'Identifier' && node.name.name === 'toString') return true;
115
+ if (node.children) return node.children.some((c: any) => hasStringHints(c));
116
+ if (node.arguments) return (node.arguments as any[]).some(a => hasStringHints(a));
117
+ if (node.left && node.right) return hasStringHints(node.left) || hasStringHints(node.right);
118
+ if (node.expression) return hasStringHints(node.expression);
119
+ return false;
120
+ })(args[0]);
121
+
122
+ const seededTotal = (args.length < 2)
123
+ ? (containsStringHints ? { type: 'String', singleton: true } as TypeInfo : itemType)
124
+ : totalType;
125
+ let aggregatorCtx = context
126
+ .withSystemVariable('$this', itemType)
127
+ .withSystemVariable('$index', { type: 'Integer', singleton: true })
128
+ .withSystemVariable('$total', seededTotal);
129
+
130
+ const firstPass = await aggregatorCtx.analyzeNode(args[0]!);
131
+ diagnostics.push(...firstPass.diagnostics);
132
+
133
+ // If no init provided, refine $total type to aggregator result and re-analyze aggregator
134
+ if (args.length < 2) {
135
+ aggregatorCtx = aggregatorCtx.withSystemVariable('$total', firstPass.type);
136
+ const secondPass = await aggregatorCtx.analyzeNode(args[0]!);
137
+ diagnostics.push(...secondPass.diagnostics);
138
+ return { type: secondPass.type, diagnostics, context };
139
+ }
140
+
141
+ return { type: firstPass.type, diagnostics, context };
142
+ }
143
+
144
+ return { type: context.inputType, diagnostics, context };
145
+ }
146
+ };
@@ -1,7 +1,7 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { RuntimeContextManager } from '../interpreter';
4
- import { box, unbox } from '../boxing';
3
+ import { RuntimeContextManager } from '../interpreter/runtime-context';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
7
  // all() requires exactly one argument (the criteria expression)
@@ -51,6 +51,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
51
51
 
52
52
  export const allFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
53
53
  name: 'all',
54
+ doesNotPropagateEmpty: true, // Returns true for empty collections
54
55
  category: ['existence'],
55
56
  description: 'Returns true if for every element in the input collection, criteria evaluates to true. Otherwise, the result is false. If the input collection is empty, the result is true.',
56
57
  examples: [
@@ -66,4 +67,4 @@ export const allFunction: FunctionDefinition & { evaluate: FunctionEvaluator } =
66
67
  result: { type: 'Boolean', singleton: true }
67
68
  }],
68
69
  evaluate
69
- };
70
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // If the input is empty, the result is true
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
24
24
 
25
25
  export const allFalseFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
26
26
  name: 'allFalse',
27
+ doesNotPropagateEmpty: true,
27
28
  category: ['existence'],
28
29
  description: 'Takes a collection of Boolean values and returns true if all the items are false. If any items are true, the result is false. If the input is empty, the result is true.',
29
30
  examples: [
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // If the input is empty, the result is true
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
24
24
 
25
25
  export const allTrueFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
26
26
  name: 'allTrue',
27
+ doesNotPropagateEmpty: true,
27
28
  category: ['existence'],
28
29
  description: 'Takes a collection of Boolean values and returns true if all the items are true. If any items are false, the result is false. If the input is empty, the result is true.',
29
30
  examples: [
@@ -1,7 +1,7 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
7
  // Three-valued logic implementation
@@ -30,6 +30,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
30
30
  export const andOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
31
31
  symbol: 'and',
32
32
  name: 'and',
33
+ doesNotPropagateEmpty: true, // Three-valued logic: false and empty = false
33
34
  category: ['logical'],
34
35
  precedence: PRECEDENCE.AND,
35
36
  associativity: 'left',
@@ -1,5 +1,5 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
- import { box, unbox } from '../boxing';
2
+ import { box, unbox } from '../interpreter/boxing';
3
3
 
4
4
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
5
5
  // Empty input returns false per spec
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
24
24
 
25
25
  export const anyFalseFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
26
26
  name: 'anyFalse',
27
+ doesNotPropagateEmpty: true,
27
28
  category: ['existence'],
28
29
  description: 'Takes a collection of Boolean values and returns true if any of the items are false. If all the items are true, or if the input is empty, the result is false.',
29
30
  examples: [
@@ -1,5 +1,5 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
- import { box, unbox } from '../boxing';
2
+ import { box, unbox } from '../interpreter/boxing';
3
3
 
4
4
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
5
5
  // Empty input returns false per spec
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
24
24
 
25
25
  export const anyTrueFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
26
26
  name: 'anyTrue',
27
+ doesNotPropagateEmpty: true,
27
28
  category: ['existence'],
28
29
  description: 'Takes a collection of Boolean values and returns true if any of the items are true. If all the items are false, or if the input is empty, the result is false.',
29
30
  examples: [
@@ -0,0 +1,58 @@
1
+ import type { FunctionDefinition, FunctionEvaluator, ASTNode, RuntimeContext, NodeEvaluator } from '../types';
2
+ import type { FHIRPathValue } from '../interpreter/boxing';
3
+ import { NodeType, isIdentifierNode } from '../types';
4
+ import { evaluate as asOperatorEvaluate } from './as-operator';
5
+
6
+ const asEvaluator: FunctionEvaluator = async (
7
+ input: FHIRPathValue[],
8
+ context: RuntimeContext,
9
+ args: ASTNode[],
10
+ evaluator: NodeEvaluator
11
+ ) => {
12
+ // as() function takes one argument - the type name
13
+ if (args.length !== 1) {
14
+ return { value: [], context };
15
+ }
16
+
17
+ const typeArg = args[0];
18
+ if (!typeArg) {
19
+ return { value: [], context };
20
+ }
21
+
22
+ // Extract type name from the argument AST node
23
+ let typeName: string;
24
+
25
+ if (isIdentifierNode(typeArg)) {
26
+ typeName = typeArg.name;
27
+ } else {
28
+ // For other node types, try to get the name
29
+ throw new Error(`as() requires a type name as argument, got ${typeArg.type}`);
30
+ }
31
+
32
+ // Use the as operator implementation with the type name
33
+ return asOperatorEvaluate(input, context, input, [typeName]);
34
+ };
35
+
36
+ export { asEvaluator };
37
+
38
+ export const asFunction: FunctionDefinition & { evaluate: typeof asEvaluator } = {
39
+ name: 'as',
40
+ category: ['type'],
41
+ description: 'Casts the input to the specified type, returning empty if the cast fails',
42
+ examples: ['Patient.name.as(HumanName)', '"hello".as(String)', '5.as(Integer)'],
43
+ signatures: [
44
+ {
45
+ name: 'as-type-cast',
46
+ parameters: [{
47
+ name: 'type',
48
+ type: { type: 'Any', singleton: true },
49
+ expression: true,
50
+ typeReference: true
51
+ }],
52
+ input: { type: 'Any', singleton: false },
53
+ result: { type: 'Any', singleton: false }
54
+ }
55
+ ],
56
+ doesNotPropagateEmpty: false,
57
+ evaluate: asEvaluator
58
+ };
@@ -1,7 +1,8 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
+ import { isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
5
6
 
6
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
8
  // 'as' operator performs type casting/filtering
@@ -10,29 +11,66 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
10
11
  return { value: [], context };
11
12
  }
12
13
 
13
- // TODO: Implement proper FHIRPath type casting
14
- // For now, just return the value as-is if it matches the type
15
14
  const results: any[] = [];
16
- const typeName = right[0]; // Should be a type name
15
+ const typeName = right[0] as string; // Should be a type name
17
16
 
18
- for (const item of left) {
17
+ for (const boxedItem of left) {
18
+ const item = unbox(boxedItem);
19
19
  let matches = false;
20
- switch (typeName) {
21
- case 'String':
22
- matches = typeof item === 'string';
23
- break;
24
- case 'Boolean':
25
- matches = typeof item === 'boolean';
26
- break;
27
- case 'Integer':
28
- matches = Number.isInteger(item);
29
- break;
30
- case 'Decimal':
31
- matches = typeof item === 'number';
32
- break;
20
+
21
+ // Check typeInfo first if available
22
+ if (boxedItem?.typeInfo) {
23
+ // If we have a ModelProvider, use it for accurate type checking (handles subtypes)
24
+ if (context.modelProvider) {
25
+ const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as any);
26
+ matches = matchingType !== undefined;
27
+ } else {
28
+ // Without ModelProvider, just check exact match
29
+ matches = boxedItem.typeInfo.type === typeName;
30
+ }
31
+ } else {
32
+ // Fallback to primitive type checking
33
+ switch (typeName) {
34
+ case 'String':
35
+ matches = typeof item === 'string';
36
+ break;
37
+ case 'Boolean':
38
+ matches = typeof item === 'boolean';
39
+ break;
40
+ case 'Integer':
41
+ matches = typeof item === 'number' && Number.isInteger(item);
42
+ break;
43
+ case 'Decimal':
44
+ matches = typeof item === 'number';
45
+ break;
46
+ case 'Date':
47
+ // Check if it's a FHIRDate instance or has Date typeInfo
48
+ if (item && typeof item === 'object') {
49
+ matches = isFHIRDate(item) || (item as any).kind === 'FHIRDate';
50
+ }
51
+ break;
52
+ case 'DateTime':
53
+ // Check if it's a FHIRDateTime instance or has DateTime typeInfo
54
+ if (item && typeof item === 'object') {
55
+ matches = isFHIRDateTime(item) || (item as any).kind === 'FHIRDateTime';
56
+ }
57
+ break;
58
+ case 'Time':
59
+ // Check if it's a FHIRTime instance or has Time typeInfo
60
+ if (item && typeof item === 'object') {
61
+ matches = isFHIRTime(item) || (item as any).kind === 'FHIRTime';
62
+ }
63
+ break;
64
+ default:
65
+ // For complex types, check resourceType
66
+ if (item && typeof item === 'object' && 'resourceType' in item) {
67
+ matches = item.resourceType === typeName;
68
+ }
69
+ }
33
70
  }
71
+
34
72
  if (matches) {
35
- results.push(item);
73
+ results.push(boxedItem);
36
74
  }
37
75
  }
38
76
 
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // ceiling() takes no arguments
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator, TypeInfo } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  if (args.length !== 0) {
@@ -22,9 +22,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
22
22
  parentTypeInfo = boxedItem.typeInfo;
23
23
  } else if (modelProvider && 'resourceType' in item && typeof item.resourceType === 'string') {
24
24
  // Try to get type info from resourceType (use cached version)
25
- parentTypeInfo = 'getTypeFromCache' in modelProvider
26
- ? (modelProvider as any).getTypeFromCache(item.resourceType)
27
- : undefined;
25
+ parentTypeInfo = await modelProvider.getType(item.resourceType);
28
26
  }
29
27
 
30
28
  // Collect all child properties
@@ -94,5 +92,16 @@ export const childrenFunction: FunctionDefinition & { evaluate: FunctionEvaluato
94
92
  parameters: [],
95
93
  result: { type: 'Any', singleton: false }
96
94
  }],
97
- evaluate
95
+ evaluate,
96
+ async inferResultType(analyzer, node, inputType) {
97
+ const modelProvider = (analyzer as any).modelProvider;
98
+ if (inputType && modelProvider && 'getChildrenType' in modelProvider) {
99
+ const childrenType = await modelProvider.getChildrenType(inputType);
100
+ if (childrenType) {
101
+ return childrenType;
102
+ }
103
+ }
104
+ // Fallback to Any collection
105
+ return { type: 'Any', singleton: false };
106
+ }
98
107
  };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // combine() requires exactly one argument (the other collection)
@@ -16,8 +16,10 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
16
16
 
17
17
  // Evaluate the argument expression
18
18
  // The argument should be evaluated in the root context, not the current input
19
- // Use context.input which contains the root data
20
- const otherResult = await evaluator(otherArg, context.input || [], context);
19
+ // Use the original context data
20
+ const rootInput = context.variables?.['%context'] || context.input || [];
21
+ const rootInputArray = Array.isArray(rootInput) ? rootInput : [rootInput];
22
+ const otherResult = await evaluator(otherArg, rootInputArray, context);
21
23
  const otherCollection = otherResult.value;
22
24
 
23
25
  // Merge the input and other collections into a single collection
@@ -29,6 +31,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
29
31
 
30
32
  export const combineFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
31
33
  name: 'combine',
34
+ doesNotPropagateEmpty: true, // Combine accepts empty collections as valid input
32
35
  category: ['collection'],
33
36
  description: 'Merge the input and other collections into a single collection without eliminating duplicate values. Combining an empty collection with a non-empty collection will return the non-empty collection. There is no expectation of order in the resulting collection.',
34
37
  examples: [
@@ -1,23 +1,22 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
7
  // Combine operator concatenates all values as strings
8
- const leftStr = left.map(v => String(unbox(v))).join('');
9
- const rightStr = right.map(v => String(unbox(v))).join('');
10
-
11
- if (leftStr === '' && rightStr === '') {
12
- return { value: [], context };
13
- }
8
+ // Empty collections are treated as empty string
9
+ const leftStr = left.length === 0 ? '' : left.map(v => String(unbox(v))).join('');
10
+ const rightStr = right.length === 0 ? '' : right.map(v => String(unbox(v))).join('');
14
11
 
12
+ // Always return a string, even if both are empty
15
13
  return { value: [box(leftStr + rightStr, { type: 'String', singleton: true })], context };
16
14
  };
17
15
 
18
16
  export const combineOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
19
17
  symbol: '&',
20
18
  name: 'combine',
19
+ doesNotPropagateEmpty: true, // Treats empty as empty string, always returns a string
21
20
  category: ['string'],
22
21
  precedence: PRECEDENCE.ADDITIVE,
23
22
  associativity: 'left',