@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
@@ -0,0 +1,66 @@
1
+ // dayOf() function - Extracts day component from Date or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRDate, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const dayOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // dayOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('dayOf', 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('dayOf', 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 Date or DateTime
31
+ if (isFHIRDate(value) || isFHIRDateTime(value)) {
32
+ // Check if day component is present
33
+ if (value.day === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the day as an Integer (1-31)
38
+ return {
39
+ value: [box(value.day, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Date or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const dayOfFunction: FunctionDefinition & { evaluate: typeof dayOfEvaluator } = {
49
+ name: 'dayOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the day component of a Date or DateTime value (1-31)',
52
+ examples: [
53
+ '@2014-01-05.dayOf()',
54
+ '@2014-01-05T10:30:00.dayOf()',
55
+ 'Patient.birthDate.dayOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'dayOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: dayOfEvaluator
66
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Decimal boundary functions for FHIRPath
3
+ *
4
+ * Implements lowBoundary and highBoundary for decimal values according to the FHIRPath specification.
5
+ * These functions return the least or greatest possible value at a specified precision.
6
+ */
7
+
8
+ /**
9
+ * Maximum supported precision for decimal values (FHIRPath requires at least 8)
10
+ */
11
+ const MAX_DECIMAL_PRECISION = 8;
12
+
13
+ /**
14
+ * Get the low boundary of a decimal value at the specified precision
15
+ *
16
+ * Returns the least possible value of the input to the specified precision.
17
+ * For the given precision, returns the truncated value minus half the step size.
18
+ *
19
+ * @param value The decimal value
20
+ * @param precision The number of decimal places (optional, defaults to input precision + 1)
21
+ * @returns The low boundary value, or null if precision is invalid
22
+ */
23
+ export function getDecimalLowBoundary(value: number, precision?: number): number | null {
24
+ // Default precision is the input's precision (for decimals) or 1 (for integers)
25
+ if (precision === undefined) {
26
+ const str = value.toString();
27
+ const decimalIndex = str.indexOf('.');
28
+ const inputPrecision = decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
29
+ // For integers, default to precision 1; for decimals, use their precision
30
+ precision = inputPrecision === 0 ? 1 : Math.min(inputPrecision, MAX_DECIMAL_PRECISION);
31
+ }
32
+
33
+ // Invalid precision returns null (empty in FHIRPath)
34
+ if (precision < 0 || precision > MAX_DECIMAL_PRECISION) {
35
+ return null;
36
+ }
37
+
38
+ // For precision 0 (integer precision)
39
+ if (precision === 0) {
40
+ // For integers at precision 0, return floor - 1 for positive, ceiling + 1 for negative
41
+ // 1.lowBoundary(0) = 0
42
+ // -1.lowBoundary(0) = -2
43
+ const truncated = value >= 0 ? Math.floor(value) : Math.ceil(value);
44
+ return truncated - 1;
45
+ }
46
+
47
+ // Calculate the factor for the given precision
48
+ const factor = Math.pow(10, precision);
49
+ const stepSize = Math.pow(10, -precision);
50
+ const halfStep = stepSize / 2;
51
+
52
+ // Check if the original value is an integer
53
+ const isInteger = Math.floor(value) === value;
54
+
55
+ // For integers with decimal precision, the boundary is 0.5 less than the value
56
+ if (isInteger && precision > 0) {
57
+ return value - 0.5;
58
+ }
59
+
60
+ // Truncate the value to the specified precision using floor (always toward negative infinity)
61
+ const truncated = Math.floor(value * factor) / factor;
62
+
63
+ // The low boundary is the truncated value minus half a step
64
+ return truncated - halfStep;
65
+ }
66
+
67
+ /**
68
+ * Get the high boundary of a decimal value at the specified precision
69
+ *
70
+ * Returns the greatest possible value of the input to the specified precision.
71
+ * For the given precision, returns the ceiling value plus half the step size.
72
+ *
73
+ * @param value The decimal value
74
+ * @param precision The number of decimal places (optional, defaults to input precision + 1)
75
+ * @returns The high boundary value, or null if precision is invalid
76
+ */
77
+ export function getDecimalHighBoundary(value: number, precision?: number): number | null {
78
+ // Default precision is the input's precision (for decimals) or 1 (for integers)
79
+ if (precision === undefined) {
80
+ const str = value.toString();
81
+ const decimalIndex = str.indexOf('.');
82
+ const inputPrecision = decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
83
+ // For integers, default to precision 1; for decimals, use their precision
84
+ precision = inputPrecision === 0 ? 1 : Math.min(inputPrecision, MAX_DECIMAL_PRECISION);
85
+ }
86
+
87
+ // Invalid precision returns null (empty in FHIRPath)
88
+ if (precision < 0 || precision > MAX_DECIMAL_PRECISION) {
89
+ return null;
90
+ }
91
+
92
+ // For precision 0 (integer precision)
93
+ if (precision === 0) {
94
+ // For integers at precision 0, return ceiling + 1 for positive, floor - 1 for negative
95
+ // 1.highBoundary(0) = 2
96
+ // -1.highBoundary(0) = -2
97
+ const ceiling = value >= 0 ? Math.ceil(value) : Math.floor(value);
98
+ return ceiling + 1;
99
+ }
100
+
101
+ // Calculate the factor for the given precision
102
+ const factor = Math.pow(10, precision);
103
+ const stepSize = Math.pow(10, -precision);
104
+ const halfStep = stepSize / 2;
105
+
106
+ // Check if the original value is an integer
107
+ const isInteger = Math.floor(value) === value;
108
+
109
+ // For integers with decimal precision, the boundary is 0.5 more than the value
110
+ if (isInteger && precision > 0) {
111
+ return value + 0.5;
112
+ }
113
+
114
+ // Ceiling the value to the specified precision (always toward positive infinity)
115
+ const ceiling = Math.ceil(value * factor) / factor;
116
+
117
+ // The high boundary is the ceiling value plus half a step
118
+ return ceiling + halfStep;
119
+ }
120
+
121
+ /**
122
+ * Format a decimal value to a specific number of decimal places
123
+ * Ensures trailing zeros are preserved in the output string
124
+ */
125
+ export function formatDecimalWithPrecision(value: number, precision: number): string {
126
+ // Handle special case for precision 0
127
+ if (precision === 0) {
128
+ return String(Math.round(value));
129
+ }
130
+
131
+ // Use toFixed to ensure the right number of decimal places
132
+ return value.toFixed(precision);
133
+ }
@@ -1,20 +1,39 @@
1
- import type { FunctionDefinition, LiteralNode } from '../types';
1
+ import type { FunctionDefinition, LiteralNode, AnalysisContext, InternalAnalysisResult } 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
+ import { DiagnosticSeverity } from '../types';
7
+ import { toDiagnostic } from '../errors';
6
8
 
7
9
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
8
10
  if (args.length < 1) {
9
11
  throw Errors.invalidOperation('defineVariable requires at least 1 argument');
10
12
  }
11
13
 
12
- const nameNode = args[0] as LiteralNode;
13
- if (nameNode.valueType !== 'string') {
14
- throw Errors.invalidOperation('Variable name must be a string');
14
+ let varName: string;
15
+
16
+ // Check if first argument is a literal
17
+ const nameArg = args[0];
18
+ if (nameArg && nameArg.type === 'Literal' && (nameArg as LiteralNode).valueType === 'string') {
19
+ // Fast path: literal string
20
+ varName = (nameArg as LiteralNode).value as string;
21
+ } else {
22
+ // Slow path: evaluate expression to get name
23
+ const nameResult = await evaluator(nameArg!, input, context);
24
+ if (nameResult.value.length === 0) {
25
+ throw Errors.invalidOperation('Variable name expression evaluated to empty');
26
+ }
27
+ const firstValue = nameResult.value[0];
28
+ if (!firstValue) {
29
+ throw Errors.invalidOperation('Variable name expression evaluated to empty');
30
+ }
31
+ const nameValue = unbox(firstValue);
32
+ if (typeof nameValue !== 'string') {
33
+ throw Errors.invalidOperation('Variable name must evaluate to a string');
34
+ }
35
+ varName = nameValue;
15
36
  }
16
-
17
- const varName = nameNode.value as string;
18
37
 
19
38
  let value: any[];
20
39
 
@@ -23,29 +42,30 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
23
42
  value = input;
24
43
  } else {
25
44
  // Two arguments: defineVariable(name, value) - evaluate value expression
26
- const tempContext = RuntimeContextManager.setVariable(context, '$this', input);
45
+ // $this should be set to the input collection (unboxed to avoid double-boxing)
46
+ const unboxedInput = input.map(v => unbox(v));
47
+ const tempContext = RuntimeContextManager.setVariable(context, '$this', unboxedInput, true);
27
48
  const valueExpr = args[1];
28
49
  if (!valueExpr) {
29
50
  throw Errors.invalidOperation('defineVariable requires a value expression');
30
51
  }
52
+
31
53
  const valueResult = await evaluator(valueExpr, input, tempContext);
32
54
  value = valueResult.value;
33
55
  }
34
56
 
35
57
  // Set the variable using RuntimeContextManager (handles prefixes and checks)
58
+ // This will throw an error if the variable is already defined (per spec)
36
59
  const newContext = RuntimeContextManager.setVariable(context, varName, value);
37
-
38
- // If newContext is same as context, variable already existed - return empty
39
- if (newContext === context) {
40
- return { value: [], context };
41
- }
42
60
 
61
+
43
62
  // Pass through input unchanged
44
63
  return { value: input, context: newContext };
45
64
  };
46
65
 
47
66
  export const defineVariableFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
48
67
  name: 'defineVariable',
68
+ doesNotPropagateEmpty: true, // defineVariable should always execute to set the variable
49
69
  category: ['context'],
50
70
  description: 'Defines a variable in the evaluation context',
51
71
  examples: [
@@ -58,9 +78,102 @@ export const defineVariableFunction: FunctionDefinition & { evaluate: FunctionEv
58
78
  input: { type: 'Any', singleton: false },
59
79
  parameters: [
60
80
  { name: 'name', type: { type: 'String', singleton: true } },
61
- { name: 'value', type: { type: 'Any', singleton: false }, optional: true },
81
+ { name: 'value', type: { type: 'Any', singleton: false }, optional: true, expression: true },
62
82
  ],
63
83
  result: { type: 'Any', singleton: false },
64
84
  }],
65
- evaluate
66
- };
85
+ evaluate,
86
+ async inferResultType(analyzer, node, inputType) {
87
+ // defineVariable returns its input type unchanged
88
+ return inputType || { type: 'Any', singleton: false };
89
+ },
90
+ /**
91
+ * Analysis-time behavior for defineVariable.
92
+ * Adds the variable to the context for downstream expressions.
93
+ */
94
+ async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
95
+ const diagnostics: any[] = [];
96
+
97
+ // Validate we have at least one argument
98
+ if (args.length < 1) {
99
+ return {
100
+ type: context.inputType,
101
+ diagnostics: [{
102
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
103
+ message: 'defineVariable requires at least 1 argument',
104
+ severity: DiagnosticSeverity.Error
105
+ }]
106
+ };
107
+ }
108
+
109
+ // First argument: variable name (can be literal or expression)
110
+ const nameNode = args[0];
111
+ let varName: string | undefined;
112
+ let isDynamic = false;
113
+
114
+ if (nameNode && nameNode.type === 'Literal' && (nameNode as LiteralNode).valueType === 'string') {
115
+ // Static variable name - full analysis
116
+ varName = (nameNode as LiteralNode).value as string;
117
+
118
+ // Check if variable already exists
119
+ if (context.userVariables.has(varName)) {
120
+ // Flag as warning in analysis; runtime enforces error with proper code (FP6009)
121
+ diagnostics.push({
122
+ range: nameNode.range,
123
+ message: `Variable '${varName}' is already defined`,
124
+ severity: DiagnosticSeverity.Warning
125
+ });
126
+ return { type: context.inputType, diagnostics, context };
127
+ }
128
+ } else {
129
+ // Dynamic variable name - limited analysis
130
+ isDynamic = true;
131
+ diagnostics.push({
132
+ range: nameNode?.range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
133
+ message: 'Dynamic variable name: cannot validate variable references at compile time',
134
+ severity: DiagnosticSeverity.Warning
135
+ });
136
+
137
+ // Still analyze the expression for other errors
138
+ if (nameNode) {
139
+ const nameResult = await context.analyzeNode(nameNode);
140
+ diagnostics.push(...nameResult.diagnostics);
141
+
142
+ // Check if it evaluates to string type
143
+ if (nameResult.type.type !== 'String') {
144
+ diagnostics.push({
145
+ range: nameNode.range,
146
+ message: 'Variable name expression must evaluate to String type',
147
+ severity: DiagnosticSeverity.Error
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ // Determine the type of the variable
154
+ let varType = context.inputType;
155
+
156
+ if (args.length >= 2 && args[1]) {
157
+ // If value expression provided, analyze it to get its type
158
+ const valueResult = await context.analyzeNode(args[1]);
159
+ diagnostics.push(...valueResult.diagnostics);
160
+ varType = valueResult.type;
161
+ }
162
+
163
+ // Return with modified context
164
+ // For static names, add the variable to context
165
+ // For dynamic names, mark that dynamic variables exist
166
+ let resultContext = context;
167
+ if (varName) {
168
+ resultContext = context.withUserVariable(varName, varType);
169
+ } else if (isDynamic) {
170
+ resultContext = context.withDynamicVariables();
171
+ }
172
+
173
+ return {
174
+ type: context.inputType, // defineVariable returns input unchanged
175
+ diagnostics,
176
+ context: resultContext
177
+ };
178
+ }
179
+ };
@@ -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
  // Use Set to track unique values based on unboxed values
@@ -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
  if (left.length === 0 || right.length === 0) {
@@ -1,9 +1,10 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { divideQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { divideQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
+ import { Errors } from '../errors';
7
8
 
8
9
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
10
  if (left.length === 0 || right.length === 0) {
@@ -20,14 +21,18 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
20
21
  // Check if both are quantities
21
22
  if (l && typeof l === 'object' && 'unit' in l &&
22
23
  r && typeof r === 'object' && 'unit' in r) {
23
- const result = divideQuantities(l as QuantityValue, r as QuantityValue);
24
+ const rightQuantity = r as QuantityValue;
25
+ if (rightQuantity.value === 0) {
26
+ throw Errors.divisionByZero();
27
+ }
28
+ const result = divideQuantities(l as QuantityValue, rightQuantity);
24
29
  return { value: result ? [box(result, { type: 'Quantity', singleton: true })] : [], context };
25
30
  }
26
31
 
27
32
  // Handle quantity / number
28
33
  if (l && typeof l === 'object' && 'unit' in l && typeof r === 'number') {
29
34
  if (r === 0) {
30
- return { value: [], context };
35
+ throw Errors.divisionByZero();
31
36
  }
32
37
  const q = l as QuantityValue;
33
38
  return { value: [box({ value: q.value / r, unit: q.unit }, { type: 'Quantity', singleton: true })], context };
@@ -36,7 +41,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
36
41
  // Handle numeric division
37
42
  if (typeof l === 'number' && typeof r === 'number') {
38
43
  if (r === 0) {
39
- return { value: [], context };
44
+ throw Errors.divisionByZero();
40
45
  }
41
46
  return { value: [box(l / r, { type: 'Any', singleton: true })], context };
42
47
  }
@@ -51,7 +56,7 @@ export const divideOperator: OperatorDefinition & { evaluate: OperationEvaluator
51
56
  category: ['arithmetic'],
52
57
  precedence: PRECEDENCE.MULTIPLICATIVE,
53
58
  associativity: 'left',
54
- description: 'Divides the left operand by the right operand (supported for Integer, Decimal, and Quantity). The result is always Decimal, even if inputs are both Integer. Division by zero returns empty.',
59
+ description: 'Divides the left operand by the right operand (supported for Integer, Decimal, and Quantity). The result is always Decimal, even if inputs are both Integer. Division by zero throws an error.',
55
60
  examples: ['10 / 2', '7.5 / 1.5', '12 \'cm2\' / 3 \'cm\'', '12 / 0'],
56
61
  signatures: [
57
62
  {
@@ -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
  // Note: The dot operator is special and is typically handled directly in the interpreter
8
8
  // because it needs to evaluate its operands in sequence, not in parallel
@@ -1,25 +1,34 @@
1
- import type { FunctionDefinition } from '../types';
2
- import type { FunctionEvaluator } from '../types';
3
- import { box, unbox } from '../boxing';
1
+ import type { FunctionDefinition } from "../types";
2
+ import type { FunctionEvaluator } from "../types";
3
+ import { box, unbox } from "../interpreter/boxing";
4
4
 
5
- export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
- return {
7
- value: [box(input.length === 0, { type: 'Boolean', singleton: true })],
8
- context
5
+ export const evaluate: FunctionEvaluator = async (
6
+ input,
7
+ context,
8
+ args,
9
+ evaluator,
10
+ ) => {
11
+ return {
12
+ value: [box(input.length === 0, { type: "Boolean", singleton: true })],
13
+ context,
9
14
  };
10
15
  };
11
16
 
12
- export const emptyFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
13
- name: 'empty',
14
- category: ['collection', 'logical'],
15
- description: 'Returns true if the collection is empty',
16
- examples: ['Patient.name.empty()'],
17
- signatures: [{
18
-
19
- name: 'empty',
20
- input: { type: 'Any', singleton: false },
21
- parameters: [],
22
- result: { type: 'Boolean', singleton: true },
23
- }],
24
- evaluate
25
- };
17
+ export const emptyFunction: FunctionDefinition & {
18
+ evaluate: FunctionEvaluator;
19
+ } = {
20
+ name: "empty",
21
+ doesNotPropagateEmpty: true,
22
+ category: ["collection", "logical"],
23
+ description: "Returns true if the collection is empty",
24
+ examples: ["Patient.name.empty()"],
25
+ signatures: [
26
+ {
27
+ name: "empty",
28
+ input: { type: "Any", singleton: false },
29
+ parameters: [],
30
+ result: { type: "Boolean", singleton: true },
31
+ },
32
+ ],
33
+ evaluate,
34
+ };
@@ -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
  // Validate arguments
@@ -36,6 +36,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
36
36
  }
37
37
  const suffixResult = await evaluator(args[0], input, context);
38
38
 
39
+ // If suffix is empty, propagate empty
40
+ if (suffixResult.value.length === 0) {
41
+ return { value: [], context };
42
+ }
43
+
39
44
  // Validate that suffix is a singleton string
40
45
  if (suffixResult.value.length !== 1) {
41
46
  throw Errors.invalidOperation('endsWith suffix argument must be a single value');
@@ -1,36 +1,19 @@
1
- import type { OperatorDefinition } from '../types';
1
+ import type { OperatorDefinition, OperationEvaluator } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
- import type { OperationEvaluator } from '../types';
4
- import { equalQuantities, compareQuantities, type QuantityValue } from '../quantity-value';
5
- import { box, unbox } from '../boxing';
3
+ import { box } from '../interpreter/boxing';
4
+ import { collectionsEqual } from './comparison';
6
5
 
7
6
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
8
- if (left.length === 0 || right.length === 0) {
9
- return { value: [], context };
10
- }
7
+ // Use the unified comparison system
8
+ const result = collectionsEqual(left, right);
11
9
 
12
- const boxedL = left[0];
13
- const boxedR = right[0];
14
-
15
- if (!boxedL || !boxedR) {
10
+ // null means incomparable (returns empty)
11
+ if (result === null) {
16
12
  return { value: [], context };
17
13
  }
18
14
 
19
- const l = unbox(boxedL);
20
- const r = unbox(boxedR);
21
-
22
- // Check if both are quantities
23
- if (l && typeof l === 'object' && 'unit' in l &&
24
- r && typeof r === 'object' && 'unit' in r) {
25
- const comparison = compareQuantities(l as QuantityValue, r as QuantityValue);
26
- // If quantities are incomparable (different dimensions), return empty
27
- if (comparison === null) {
28
- return { value: [], context };
29
- }
30
- return { value: [box(comparison === 0, { type: 'Boolean', singleton: true })], context };
31
- }
32
-
33
- return { value: [box(l === r, { type: 'Boolean', singleton: true })], context };
15
+ // Return the boolean result
16
+ return { value: [box(result, { type: 'Boolean', singleton: true })], context };
34
17
  };
35
18
 
36
19
  export const equalOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -41,11 +24,19 @@ export const equalOperator: OperatorDefinition & { evaluate: OperationEvaluator
41
24
  associativity: 'left',
42
25
  description: 'Returns true if the left collection is equal to the right collection. For single items, comparison is type-specific. For collections, comparison is order-dependent.',
43
26
  examples: ['name = "John"', 'Patient.name.given = "John"', '5 = 5', '@2018-03-01 = @2018-03-01'],
44
- signatures: [{
45
- name: 'equal',
46
- left: { type: 'Any', singleton: true },
47
- right: { type: 'Any', singleton: true },
48
- result: { type: 'Boolean', singleton: true },
49
- }],
27
+ signatures: [
28
+ {
29
+ name: 'equal',
30
+ left: { type: 'Any', singleton: true },
31
+ right: { type: 'Any', singleton: true },
32
+ result: { type: 'Boolean', singleton: true },
33
+ },
34
+ {
35
+ name: 'equal',
36
+ left: { type: 'Any', singleton: false },
37
+ right: { type: 'Any', singleton: false },
38
+ result: { type: 'Boolean', singleton: true },
39
+ }
40
+ ],
50
41
  evaluate
51
42
  };