@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
@@ -1,64 +1,23 @@
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 } from '../interpreter/boxing';
5
+ import { collectionsEquivalent } from './comparison';
5
6
 
6
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
- // Empty collections are equivalent
8
- if (left.length === 0 && right.length === 0) {
9
- return { value: [box(true, { type: 'Boolean', singleton: true })], context };
10
- }
11
-
12
- // Different sizes are not equivalent
13
- if (left.length !== right.length) {
14
- return { value: [box(false, { type: 'Boolean', singleton: true })], context };
15
- }
16
-
17
- // For single items, check type-specific equivalence
18
- if (left.length === 1 && right.length === 1) {
19
- const boxedl = left[0];
20
- if (!boxedl) return { value: [], context };
21
- const l = unbox(boxedl);
22
- const boxedr = right[0];
23
- if (!boxedr) return { value: [], context };
24
- const r = unbox(boxedr);
25
-
26
- // String equivalence: case-insensitive with normalized whitespace
27
- if (typeof l === 'string' && typeof r === 'string') {
28
- const normalizeString = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
29
- return { value: [box(normalizeString(l) === normalizeString(r), { type: 'Boolean', singleton: true })], context };
30
- }
31
-
32
- // Number equivalence (for Integer and Decimal)
33
- if (typeof l === 'number' && typeof r === 'number') {
34
- // For decimals with different precision, round to least precise
35
- // This is a simplified implementation
36
- return { value: [box(Math.abs(l - r) < Number.EPSILON, { type: 'Boolean', singleton: true })], context };
37
- }
38
-
39
- // Boolean equivalence
40
- if (typeof l === 'boolean' && typeof r === 'boolean') {
41
- return { value: [box(l === r, { type: 'Boolean', singleton: true })], context };
42
- }
43
-
44
- // For complex types and other cases, use equality for now
45
- // TODO: Implement full equivalence logic for Date/DateTime/Time and complex types
46
- return { value: [box(l === r, { type: 'Boolean', singleton: true })], context };
47
- }
8
+ // Use the new collectionsEquivalent function from comparison.ts
9
+ const result = collectionsEquivalent(left, right);
48
10
 
49
- // For multiple items, comparison is order-independent
50
- // Create sorted copies for comparison
51
- // TODO: Implement proper order-independent comparison with recursive equivalence
52
- const leftSorted = [...left].sort();
53
- const rightSorted = [...right].sort();
54
-
55
- for (let i = 0; i < leftSorted.length; i++) {
56
- if (leftSorted[i] !== rightSorted[i]) {
57
- return { value: [box(false, { type: 'Boolean', singleton: true })], context };
58
- }
11
+ // null result means incomparable - return empty collection
12
+ if (result === null) {
13
+ return { value: [], context };
59
14
  }
60
15
 
61
- return { value: [box(true, { type: 'Boolean', singleton: true })], context };
16
+ // Return boolean result
17
+ return {
18
+ value: [box(result, { type: 'Boolean', singleton: true })],
19
+ context
20
+ };
62
21
  };
63
22
 
64
23
  export const equivalentOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -67,6 +26,7 @@ export const equivalentOperator: OperatorDefinition & { evaluate: OperationEvalu
67
26
  category: ['equality'],
68
27
  precedence: PRECEDENCE.EQUALITY,
69
28
  associativity: 'left',
29
+ doesNotPropagateEmpty: true, // Empty collections are valid operands for equivalence
70
30
  description: 'Returns true if the collections are the same. For single items: strings are compared case-insensitive with normalized whitespace, decimals are rounded to least precision, dates with different precision return false. For collections: order-independent comparison. Empty ~ empty returns true (unlike =)',
71
31
  examples: [
72
32
  "'abc' ~ 'ABC'",
@@ -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 (args.length !== 1) {
@@ -44,6 +44,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
44
44
 
45
45
  export const excludeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
46
46
  name: 'exclude',
47
+ doesNotPropagateEmpty: true, // exclude with empty argument returns the original collection
47
48
  category: ['collection'],
48
49
  description: 'Returns the set of elements that are not in the other collection. Duplicate items will not be eliminated by this function, and order will be preserved.',
49
50
  examples: [
@@ -1,8 +1,8 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
- import { RuntimeContextManager } from '../interpreter';
3
+ import { RuntimeContextManager } from '../interpreter/runtime-context';
4
4
  import { type FunctionEvaluator } from '../types';
5
- import { box, unbox } from '../boxing';
5
+ import { box, unbox } from '../interpreter/boxing';
6
6
 
7
7
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
8
8
  // No arguments - just check if input is not empty
@@ -42,6 +42,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
42
42
 
43
43
  export const existsFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
44
44
  name: 'exists',
45
+ doesNotPropagateEmpty: true,
45
46
  category: ['collection', 'logical'],
46
47
  description: 'Returns true if the collection has any items, or if any item satisfies the condition',
47
48
  examples: ['Patient.name.exists()', 'Patient.name.exists(use = "official")'],
@@ -55,4 +56,4 @@ export const existsFunction: FunctionDefinition & { evaluate: FunctionEvaluator
55
56
  result: { type: 'Boolean', singleton: true },
56
57
  }],
57
58
  evaluate
58
- };
59
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import type { FunctionEvaluator } from '../types';
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 (input.length > 0) {
@@ -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
  // floor() takes no arguments
@@ -1,9 +1,9 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { compareQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { compareQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
7
 
8
8
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
9
  if (left.length === 0 || right.length === 0) {
@@ -24,6 +24,23 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
24
24
  return { value: result !== null ? [box(result > 0, { type: 'Boolean', singleton: true })] : [], context };
25
25
  }
26
26
 
27
+ // Check if both are temporal values (Date, DateTime, Time)
28
+ if (l && typeof l === 'object' && 'kind' in l &&
29
+ r && typeof r === 'object' && 'kind' in r) {
30
+ const temporalL = l as any;
31
+ const temporalR = r as any;
32
+ const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
+ if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
+ const { compare } = await import('../complex-types/temporal');
35
+ const result = compare(temporalL, temporalR);
36
+ // null means incomparable (different precisions), returns empty
37
+ if (result === null) {
38
+ return { value: [], context };
39
+ }
40
+ return { value: [box(result > 0, { type: 'Boolean', singleton: true })], context };
41
+ }
42
+ }
43
+
27
44
  return { value: [box((l as any) > (r as any), { type: 'Boolean', singleton: true })], context };
28
45
  };
29
46
 
@@ -1,9 +1,9 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { compareQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { compareQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
7
 
8
8
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
9
  if (left.length === 0 || right.length === 0) {
@@ -24,6 +24,23 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
24
24
  return { value: result !== null ? [box(result >= 0, { type: 'Boolean', singleton: true })] : [], context };
25
25
  }
26
26
 
27
+ // Check if both are temporal values (Date, DateTime, Time)
28
+ if (l && typeof l === 'object' && 'kind' in l &&
29
+ r && typeof r === 'object' && 'kind' in r) {
30
+ const temporalL = l as any;
31
+ const temporalR = r as any;
32
+ const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
+ if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
+ const { compare } = await import('../complex-types/temporal');
35
+ const result = compare(temporalL, temporalR);
36
+ // null means incomparable (different precisions), returns empty
37
+ if (result === null) {
38
+ return { value: [], context };
39
+ }
40
+ return { value: [box(result >= 0, { type: 'Boolean', singleton: true })], context };
41
+ }
42
+ }
43
+
27
44
  return { value: [box((l as any) >= (r as any), { type: 'Boolean', singleton: true })], context };
28
45
  };
29
46
 
@@ -0,0 +1,120 @@
1
+ // highBoundary() function - Returns the greatest possible value to the specified precision
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import {
5
+ isFHIRDate, isFHIRDateTime, isFHIRTime,
6
+ getDateHighBoundary, getDateTimeHighBoundary, getTimeHighBoundary
7
+ } from '../complex-types/temporal';
8
+ import { getDecimalHighBoundary } from './decimal-boundaries';
9
+ import { Errors } from '../errors';
10
+
11
+ export const highBoundaryEvaluator: FunctionEvaluator = async (input, context, args, evaluator) => {
12
+ // highBoundary() takes optional precision parameter
13
+ if (args.length > 1) {
14
+ throw Errors.wrongArgumentCountRange('highBoundary', 0, 1, args.length);
15
+ }
16
+
17
+ // Empty input returns empty
18
+ if (input.length === 0) {
19
+ return { value: [], context };
20
+ }
21
+
22
+ // Multiple items throws error
23
+ if (input.length > 1) {
24
+ throw Errors.singletonRequired('highBoundary', input.length);
25
+ }
26
+
27
+ const boxedValue = input[0];
28
+ if (!boxedValue) {
29
+ return { value: [], context };
30
+ }
31
+
32
+ const value = unbox(boxedValue);
33
+
34
+ // Get precision if provided
35
+ let precision: number | undefined;
36
+ if (args.length === 1) {
37
+ const precisionResult = await evaluator(args[0]!, input, context);
38
+ const precisionArg = precisionResult.value;
39
+ if (precisionArg.length === 0) {
40
+ return { value: [], context };
41
+ }
42
+ if (precisionArg.length > 1) {
43
+ throw Errors.singletonRequired('highBoundary precision', precisionArg.length);
44
+ }
45
+ const precisionValue = unbox(precisionArg[0]!);
46
+ if (typeof precisionValue !== 'number' || !Number.isInteger(precisionValue)) {
47
+ throw Errors.invalidOperandType('highBoundary precision', typeof precisionValue);
48
+ }
49
+ precision = precisionValue;
50
+ }
51
+
52
+ // Handle Date
53
+ if (isFHIRDate(value)) {
54
+ const result = getDateHighBoundary(value, precision);
55
+ if (!result) {
56
+ return { value: [], context };
57
+ }
58
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
59
+ }
60
+
61
+ // Handle DateTime
62
+ if (isFHIRDateTime(value)) {
63
+ const result = getDateTimeHighBoundary(value, precision);
64
+ if (!result) {
65
+ return { value: [], context };
66
+ }
67
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
68
+ }
69
+
70
+ // Handle Time
71
+ if (isFHIRTime(value)) {
72
+ const result = getTimeHighBoundary(value, precision);
73
+ if (!result) {
74
+ return { value: [], context };
75
+ }
76
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
77
+ }
78
+
79
+ // For Decimal/Integer types
80
+ if (typeof value === 'number') {
81
+ const result = getDecimalHighBoundary(value, precision);
82
+ if (result === null) {
83
+ return { value: [], context };
84
+ }
85
+ // Determine the result type based on whether it's an integer or decimal
86
+ const isInteger = Number.isInteger(result);
87
+ return {
88
+ value: [box(result, { type: isInteger ? 'Integer' : 'Decimal', singleton: true })],
89
+ context
90
+ };
91
+ }
92
+
93
+ // Invalid type returns empty
94
+ return { value: [], context };
95
+ };
96
+
97
+ export const highBoundaryFunction: FunctionDefinition & { evaluate: typeof highBoundaryEvaluator } = {
98
+ name: 'highBoundary',
99
+ category: ['utility'],
100
+ description: 'Returns the greatest possible value of the input to the specified precision',
101
+ examples: [
102
+ '@2014.highBoundary(6)',
103
+ '@2014-01-01T08.highBoundary(17)',
104
+ '@T10:30.highBoundary(9)',
105
+ '1.587.highBoundary()',
106
+ '1.587.highBoundary(2)',
107
+ '1.highBoundary(0)'
108
+ ],
109
+ signatures: [
110
+ {
111
+ name: 'highBoundary',
112
+ input: { type: 'Any' as const, singleton: true },
113
+ parameters: [
114
+ { name: 'precision', type: { type: 'Integer' as const, singleton: true }, optional: true }
115
+ ],
116
+ result: { type: 'Any' as const, singleton: true }
117
+ }
118
+ ],
119
+ evaluate: highBoundaryEvaluator
120
+ };
@@ -0,0 +1,66 @@
1
+ // hourOf() function - Extracts hour component from Time or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRTime, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const hourOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // hourOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('hourOf', 0, args.length);
11
+ }
12
+
13
+ // Empty input returns empty
14
+ if (input.length === 0) {
15
+ return { value: [], context };
16
+ }
17
+
18
+ // Multiple items throws error
19
+ if (input.length > 1) {
20
+ throw Errors.singletonRequired('hourOf', input.length);
21
+ }
22
+
23
+ const boxedValue = input[0];
24
+ if (!boxedValue) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const value = unbox(boxedValue);
29
+
30
+ // Check if it's a Time or DateTime
31
+ if (isFHIRTime(value) || isFHIRDateTime(value)) {
32
+ // Check if hour component is present
33
+ if (value.hour === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the hour as an Integer (0-23)
38
+ return {
39
+ value: [box(value.hour, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Time or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const hourOfFunction: FunctionDefinition & { evaluate: typeof hourOfEvaluator } = {
49
+ name: 'hourOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the hour component of a Time or DateTime value (0-23)',
52
+ examples: [
53
+ '@T10:30:00.hourOf()',
54
+ '@2014-01-05T10:30:00.hourOf()',
55
+ 'Patient.birthDate.hourOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'hourOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: hourOfEvaluator
66
+ };
@@ -1,8 +1,10 @@
1
- import type { FunctionDefinition } from '../types';
2
- import { Errors } from '../errors';
1
+ import type { FunctionDefinition, AnalysisContext, InternalAnalysisResult } from '../types';
2
+ import { Errors, ErrorCodes } from '../errors';
3
3
  import type { FunctionEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
5
- import { RuntimeContextManager } from '../interpreter';
4
+ import { box, unbox } from '../interpreter/boxing';
5
+ import { RuntimeContextManager } from '../interpreter/runtime-context';
6
+ import { NodeType } from '../parser';
7
+ import { DiagnosticSeverity } from '../types';
6
8
 
7
9
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
8
10
  if (args.length < 2) {
@@ -15,7 +17,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
15
17
 
16
18
  // Check for multiple items in input collection
17
19
  if (input.length > 1) {
18
- throw Errors.invalidOperation('iif can only be used on single item or empty collections');
20
+ throw Errors.emptyNotAllowed('iif');
19
21
  }
20
22
 
21
23
  // Always evaluate condition
@@ -72,6 +74,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
72
74
 
73
75
  export const iifFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
74
76
  name: 'iif',
77
+ doesNotPropagateEmpty: true, // iif evaluates even with empty input
75
78
  category: ['control'],
76
79
  description: 'If-then-else expression (immediate if)',
77
80
  examples: ['iif(gender = "male", "Mr.", "Ms.")'],
@@ -86,5 +89,181 @@ export const iifFunction: FunctionDefinition & { evaluate: FunctionEvaluator } =
86
89
  ],
87
90
  result: { type: 'Any', singleton: false },
88
91
  }],
89
- evaluate
90
- };
92
+ evaluate,
93
+
94
+ /**
95
+ * Analysis-time behavior for iif with lazy evaluation.
96
+ * Only analyzes reachable branches when condition is a literal.
97
+ */
98
+ async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
99
+ const diagnostics: any[] = [];
100
+
101
+ // Validate argument count
102
+ if (args.length < 2) {
103
+ return {
104
+ type: { type: 'Any', singleton: false },
105
+ diagnostics: [{
106
+ message: 'iif requires at least 2 arguments',
107
+ severity: DiagnosticSeverity.Error,
108
+ code: ErrorCodes.WRONG_ARGUMENT_COUNT,
109
+ source: 'fhirpath',
110
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
111
+ }]
112
+ };
113
+ }
114
+
115
+ if (args.length > 3) {
116
+ return {
117
+ type: { type: 'Any', singleton: false },
118
+ diagnostics: [{
119
+ message: 'iif takes at most 3 arguments',
120
+ severity: DiagnosticSeverity.Error,
121
+ code: ErrorCodes.WRONG_ARGUMENT_COUNT,
122
+ source: 'fhirpath',
123
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
124
+ }]
125
+ };
126
+ }
127
+
128
+ // Analyze condition
129
+ const conditionArg = args[0];
130
+ if (!conditionArg) {
131
+ return {
132
+ type: { type: 'Any', singleton: false },
133
+ diagnostics: [{
134
+ message: 'iif requires a condition argument',
135
+ severity: DiagnosticSeverity.Error,
136
+ code: ErrorCodes.WRONG_ARGUMENT_COUNT,
137
+ source: 'fhirpath',
138
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
139
+ }]
140
+ };
141
+ }
142
+ const condResult = await context.analyzeNode(conditionArg);
143
+ diagnostics.push(...condResult.diagnostics);
144
+
145
+ // Check if condition is a literal boolean
146
+ let isLiteralTrue = false;
147
+ let isLiteralFalse = false;
148
+
149
+ if (conditionArg.type === NodeType.Literal) {
150
+ const literalValue = (conditionArg as any).value;
151
+ if (literalValue === true) {
152
+ isLiteralTrue = true;
153
+ } else if (literalValue === false) {
154
+ isLiteralFalse = true;
155
+ }
156
+ }
157
+
158
+ let trueBranchType = { type: 'Any', singleton: false } as any;
159
+ let falseBranchType = { type: 'Any', singleton: false } as any;
160
+
161
+ // Lazy evaluation: only analyze reachable branches
162
+ if (isLiteralTrue) {
163
+ // Only true branch is reachable
164
+ const trueBranch = args[1];
165
+ if (!trueBranch) {
166
+ return {
167
+ type: { type: 'Any', singleton: false },
168
+ diagnostics
169
+ };
170
+ }
171
+ const trueBranchResult = await context.analyzeNode(trueBranch);
172
+ diagnostics.push(...trueBranchResult.diagnostics);
173
+ trueBranchType = trueBranchResult.type;
174
+
175
+ // Warn about unreachable false branch
176
+ if (args.length === 3 && args[2]) {
177
+ diagnostics.push({
178
+ message: 'Unreachable code: false branch will never execute',
179
+ severity: DiagnosticSeverity.Warning,
180
+ code: ErrorCodes.UNREACHABLE_CODE,
181
+ source: 'fhirpath',
182
+ range: args[2].range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
183
+ });
184
+ }
185
+
186
+ return { type: trueBranchType, diagnostics };
187
+ } else if (isLiteralFalse) {
188
+ // Only false branch is reachable (if it exists)
189
+ // Warn about unreachable true branch
190
+ const trueBranch = args[1];
191
+ if (trueBranch) {
192
+ diagnostics.push({
193
+ message: 'Unreachable code: true branch will never execute',
194
+ severity: DiagnosticSeverity.Warning,
195
+ code: ErrorCodes.UNREACHABLE_CODE,
196
+ source: 'fhirpath',
197
+ range: trueBranch.range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
198
+ });
199
+ }
200
+
201
+ if (args.length === 3 && args[2]) {
202
+ const falseBranchResult = await context.analyzeNode(args[2]);
203
+ diagnostics.push(...falseBranchResult.diagnostics);
204
+ falseBranchType = falseBranchResult.type;
205
+ return { type: falseBranchType, diagnostics };
206
+ } else {
207
+ // No else branch, return empty
208
+ return { type: { type: 'Any', singleton: false }, diagnostics };
209
+ }
210
+ } else {
211
+ // Dynamic condition: analyze both branches
212
+ const trueBranch = args[1];
213
+ if (trueBranch) {
214
+ const trueBranchResult = await context.analyzeNode(trueBranch);
215
+ diagnostics.push(...trueBranchResult.diagnostics);
216
+ trueBranchType = trueBranchResult.type;
217
+ }
218
+
219
+ if (args.length === 3 && args[2]) {
220
+ const falseBranchResult = await context.analyzeNode(args[2]);
221
+ diagnostics.push(...falseBranchResult.diagnostics);
222
+ falseBranchType = falseBranchResult.type;
223
+
224
+ // Result type is the union of both branches
225
+ // For now, if they differ, return Any
226
+ if (trueBranchType.type === falseBranchType.type &&
227
+ trueBranchType.singleton === falseBranchType.singleton) {
228
+ return { type: trueBranchType, diagnostics };
229
+ } else if (trueBranchType.type === falseBranchType.type) {
230
+ // Same type but different singleton - result is collection
231
+ return { type: { type: trueBranchType.type, singleton: false }, diagnostics };
232
+ }
233
+ }
234
+
235
+ return { type: { type: 'Any', singleton: false }, diagnostics };
236
+ }
237
+ },
238
+
239
+ async inferResultType(analyzer, node, inputType) {
240
+ // iif returns the common type of the true and false branches
241
+ if (node.arguments.length >= 2) {
242
+ const trueBranchType = await (analyzer as any).inferType(node.arguments[1]!, inputType);
243
+ if (node.arguments.length >= 3) {
244
+ const falseBranchType = await (analyzer as any).inferType(node.arguments[2]!, inputType);
245
+ // If both branches have the same type, use that
246
+ if (trueBranchType.type === falseBranchType.type &&
247
+ trueBranchType.singleton === falseBranchType.singleton) {
248
+ return trueBranchType;
249
+ }
250
+ // If types are the same but singleton differs, return as collection
251
+ if (trueBranchType.type === falseBranchType.type) {
252
+ // One is singleton, one is collection - result must be collection
253
+ return { type: trueBranchType.type, singleton: false };
254
+ }
255
+ // Otherwise, check if one is a subtype of the other
256
+ if ((analyzer as any).isTypeCompatible(trueBranchType, falseBranchType)) {
257
+ return falseBranchType;
258
+ }
259
+ if ((analyzer as any).isTypeCompatible(falseBranchType, trueBranchType)) {
260
+ return trueBranchType;
261
+ }
262
+ } else {
263
+ // Only true branch, result can be that type or empty
264
+ return { ...trueBranchType, singleton: false };
265
+ }
266
+ }
267
+ return { type: 'Any', singleton: false };
268
+ }
269
+ };
@@ -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 for implies per spec truth table
@@ -2,7 +2,7 @@ import type { OperatorDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
3
  import { PRECEDENCE } from '../types';
4
4
  import type { OperationEvaluator } from '../types';
5
- import { box, unbox } from '../boxing';
5
+ import { box, unbox } from '../interpreter/boxing';
6
6
 
7
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
8
8
  // If left is empty, result is empty
@@ -45,6 +45,7 @@ export const inOperator: OperatorDefinition & { evaluate: OperationEvaluator } =
45
45
  category: ['membership'],
46
46
  precedence: PRECEDENCE.IN_CONTAINS,
47
47
  associativity: 'left',
48
+ doesNotPropagateEmpty: true, // Has custom empty handling per spec
48
49
  description: 'If the left operand is a collection with a single item, returns true if the item is in the right operand using equality semantics. If the left is empty, the result is empty. If the right is empty, the result is false.',
49
50
  examples: ['\'Joe\' in Patient.name.given', '5 in (1 | 2 | 3 | 4 | 5)', 'code in terminologyServer.valueset(\'my-valueset\').code'],
50
51
  signatures: [