@atomic-ehr/fhirpath 0.0.1-canary.0c6931e.20250727185306

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 (85) hide show
  1. package/README.md +473 -0
  2. package/dist/index.d.ts +462 -0
  3. package/dist/index.js +10307 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +58 -0
  6. package/src/analyzer/analyzer.ts +499 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +157 -0
  12. package/src/api/errors.ts +145 -0
  13. package/src/api/expression.ts +156 -0
  14. package/src/api/index.ts +122 -0
  15. package/src/api/inspect.ts +99 -0
  16. package/src/api/registry.ts +128 -0
  17. package/src/api/types.ts +210 -0
  18. package/src/compiler/compiler.ts +546 -0
  19. package/src/compiler/index.ts +2 -0
  20. package/src/compiler/prototype-context-adapter.ts +99 -0
  21. package/src/compiler/types.ts +24 -0
  22. package/src/index.ts +107 -0
  23. package/src/interpreter/README.md +78 -0
  24. package/src/interpreter/interpreter.ts +475 -0
  25. package/src/interpreter/types.ts +108 -0
  26. package/src/lexer/char-tables.ts +37 -0
  27. package/src/lexer/errors.ts +31 -0
  28. package/src/lexer/index.ts +5 -0
  29. package/src/lexer/lexer.ts +745 -0
  30. package/src/lexer/token.ts +104 -0
  31. package/src/lexer2/index.md +232 -0
  32. package/src/lexer2/index.perf.test.ts +68 -0
  33. package/src/lexer2/index.test.ts +549 -0
  34. package/src/lexer2/index.ts +1251 -0
  35. package/src/lexer2/notes.md +173 -0
  36. package/src/lexer2/optimization-summary.md +718 -0
  37. package/src/parser/ast-factory.ts +220 -0
  38. package/src/parser/ast.ts +144 -0
  39. package/src/parser/collection-parser.ts +89 -0
  40. package/src/parser/diagnostic-messages.ts +216 -0
  41. package/src/parser/diagnostics.ts +85 -0
  42. package/src/parser/error-reporter.ts +230 -0
  43. package/src/parser/index.ts +3 -0
  44. package/src/parser/literal-parser.ts +103 -0
  45. package/src/parser/parse-error.ts +16 -0
  46. package/src/parser/parser-error-factory.ts +141 -0
  47. package/src/parser/parser-state.ts +134 -0
  48. package/src/parser/parser.ts +1272 -0
  49. package/src/parser/pprint.ts +169 -0
  50. package/src/parser/precedence-manager.ts +64 -0
  51. package/src/parser/source-mapper.ts +248 -0
  52. package/src/parser/special-constructs.ts +142 -0
  53. package/src/parser/token-navigator.ts +110 -0
  54. package/src/parser/types.ts +60 -0
  55. package/src/parser2/index.md +177 -0
  56. package/src/parser2/index.perf.test.ts +184 -0
  57. package/src/parser2/index.test.ts +305 -0
  58. package/src/parser2/index.ts +578 -0
  59. package/src/parser2/optimization-summary.md +176 -0
  60. package/src/registry/default-analyzers.ts +257 -0
  61. package/src/registry/default-compilers.ts +31 -0
  62. package/src/registry/index.ts +96 -0
  63. package/src/registry/operations/arithmetic.ts +506 -0
  64. package/src/registry/operations/collection.ts +425 -0
  65. package/src/registry/operations/comparison.ts +432 -0
  66. package/src/registry/operations/existence.ts +703 -0
  67. package/src/registry/operations/filtering.ts +358 -0
  68. package/src/registry/operations/literals.ts +341 -0
  69. package/src/registry/operations/logical.ts +439 -0
  70. package/src/registry/operations/math.ts +128 -0
  71. package/src/registry/operations/membership.ts +132 -0
  72. package/src/registry/operations/navigation.ts +52 -0
  73. package/src/registry/operations/string.ts +507 -0
  74. package/src/registry/operations/subsetting.ts +174 -0
  75. package/src/registry/operations/type-checking.ts +162 -0
  76. package/src/registry/operations/type-conversion.ts +404 -0
  77. package/src/registry/operations/type-operators.ts +308 -0
  78. package/src/registry/operations/utility.ts +644 -0
  79. package/src/registry/registry.ts +146 -0
  80. package/src/registry/types.ts +161 -0
  81. package/src/registry/utils/evaluation-helpers.ts +93 -0
  82. package/src/registry/utils/index.ts +3 -0
  83. package/src/registry/utils/type-system.ts +173 -0
  84. package/src/runtime/context.ts +158 -0
  85. package/src/runtime/debug-context.ts +135 -0
package/src/index.ts ADDED
@@ -0,0 +1,107 @@
1
+ // Import registry to trigger operation registration
2
+ import './registry';
3
+
4
+ // Core API functions
5
+ import {
6
+ parse,
7
+ parseForEvaluation,
8
+ evaluate,
9
+ compile,
10
+ analyze,
11
+ registry,
12
+ isStandardResult,
13
+ isDiagnosticResult,
14
+ validate
15
+ } from './api';
16
+
17
+ // Default export with common operations
18
+ export default {
19
+ parse,
20
+ parseForEvaluation,
21
+ evaluate,
22
+ compile,
23
+ analyze,
24
+ registry
25
+ };
26
+
27
+ // Named exports for core functions
28
+ export {
29
+ parse,
30
+ parseForEvaluation,
31
+ evaluate,
32
+ compile,
33
+ analyze,
34
+ registry,
35
+ isStandardResult,
36
+ isDiagnosticResult,
37
+ validate
38
+ };
39
+
40
+ // Named exports for advanced usage
41
+ export { FHIRPath } from './api/builder';
42
+ export { FHIRPathError, ErrorCode } from './api/errors';
43
+
44
+ // Export types
45
+ export type {
46
+ // Core types
47
+ FHIRPathExpression,
48
+ CompiledExpression,
49
+ EvaluationContext,
50
+ CompileOptions,
51
+ AnalyzeOptions,
52
+ AnalysisResult,
53
+
54
+ // Error types
55
+ AnalysisError,
56
+ AnalysisWarning,
57
+ Location,
58
+
59
+ // Extension types
60
+ ModelProvider,
61
+ PropertyDefinition,
62
+ CustomFunction,
63
+ CustomFunctionMap,
64
+
65
+ // Registry types
66
+ RegistryAPI,
67
+ OperationMetadata,
68
+ OperationInfo,
69
+
70
+ // Builder types
71
+ FHIRPathBuilder,
72
+ FHIRPathAPI
73
+ } from './api/types';
74
+
75
+ // Export parser types
76
+ export type {
77
+ ParserOptions,
78
+ ParseResult,
79
+ ParseDiagnostic,
80
+ DiagnosticSeverity,
81
+ TextRange,
82
+ Position as TextPosition
83
+ } from './api';
84
+
85
+ // Export AST types for LSP server
86
+ export type {
87
+ ASTNode,
88
+ Position,
89
+ IdentifierNode,
90
+ BinaryNode,
91
+ UnaryNode,
92
+ UnionNode,
93
+ FunctionNode,
94
+ LiteralNode,
95
+ VariableNode,
96
+ CollectionNode,
97
+ MembershipTestNode,
98
+ TypeCastNode,
99
+ TypeReferenceNode,
100
+ IndexNode
101
+ } from './parser/ast';
102
+
103
+ export { NodeType } from './parser/ast';
104
+
105
+ // Export lexer types for LSP server
106
+ export { TokenType } from './lexer/token';
107
+ export type { Token } from './lexer/token';
@@ -0,0 +1,78 @@
1
+ # FHIRPath Interpreter
2
+
3
+ A stream-processing based FHIRPath interpreter implementation following the mental model described in `ideas/fhirpath-mental-model-3.md`.
4
+
5
+ ## Architecture
6
+
7
+ The interpreter follows the core principle that **everything is a processing node** with a uniform interface:
8
+ - **Input**: Always a collection (even single values are collections of one)
9
+ - **Context**: Variables and environment data flowing parallel to data
10
+ - **Output**: The resulting collection
11
+ - **New Context**: Potentially modified context
12
+
13
+ ## Implementation Status
14
+
15
+ ### ✅ Phase 1: Core Infrastructure
16
+ - `types.ts` - Core interfaces (EvaluationResult, Context, TypeInfo)
17
+ - `context.ts` - Context management (variables, environment)
18
+ - `interpreter.ts` - Base interpreter class with node dispatch
19
+
20
+ ### ✅ Phase 2: Simple Nodes
21
+ - **Literals**: Numbers, strings, booleans, null, collections
22
+ - **Identifiers**: Property navigation with flattening
23
+ - **Variables**: $this, $index, $total, %user-variables, %context
24
+ - **Dot Operator**: Pipeline semantics (left output → right input)
25
+
26
+ ### ✅ Phase 3: Operators
27
+ - **Arithmetic**: +, -, *, /, div, mod with singleton conversion
28
+ - **Comparison**: =, !=, <, >, <=, >= with three-valued logic
29
+ - **Logical**: and, or, not, xor, implies with three-valued logic
30
+ - **Unary**: +, -, not
31
+
32
+ ### 🚧 Phase 4: Basic Functions (Next)
33
+ - Function dispatch mechanism
34
+ - Simple value functions (first, last, count, etc.)
35
+ - Iterator functions (where, select, exists, all)
36
+
37
+ ### 📋 Phase 5: Type System (Planned)
38
+ - Type hierarchy and checking
39
+ - is/as operators
40
+ - Type conversions
41
+
42
+ ### 📋 Phase 6: Advanced Features (Planned)
43
+ - Context modification (defineVariable)
44
+ - Conditional evaluation (iif)
45
+ - Collection operations (union, intersect)
46
+ - Index operations
47
+
48
+ ## Key Design Decisions
49
+
50
+ 1. **Two-Phase Evaluation**: Control flow (top-down) and data flow (bottom-up)
51
+ 2. **Collection Semantics**: Everything is a collection, empty represents null/missing
52
+ 3. **Context Threading**: Context flows through expressions, modified by some operations
53
+ 4. **Three-Valued Logic**: Empty collections represent "unknown" in boolean operations
54
+ 5. **Singleton Rules**: Automatic conversion from single-item collections when needed
55
+
56
+ ## Usage
57
+
58
+ ```typescript
59
+ import { evaluateFHIRPath } from './interpreter/interpreter';
60
+ import { ContextManager } from './interpreter/context';
61
+
62
+ // Simple evaluation
63
+ const result = evaluateFHIRPath('Patient.name.given', patient);
64
+
65
+ // With context
66
+ const context = ContextManager.create();
67
+ const ctxWithVar = ContextManager.setVariable(context, 'threshold', [10]);
68
+ const result2 = evaluateFHIRPath('value > %threshold', data, ctxWithVar);
69
+ ```
70
+
71
+ ## Testing
72
+
73
+ Tests are organized by implementation phase in `test/interpreter.test.ts`:
74
+ - Phase 2: Simple nodes (literals, identifiers, variables, dot)
75
+ - Phase 3: Operators (arithmetic, comparison, logical)
76
+ - Phase 4: Functions (coming soon)
77
+
78
+ Run tests with: `bun test test/interpreter.test.ts`
@@ -0,0 +1,475 @@
1
+ import type {
2
+ ASTNode,
3
+ LiteralNode,
4
+ IdentifierNode,
5
+ VariableNode,
6
+ BinaryNode,
7
+ UnaryNode,
8
+ FunctionNode,
9
+ CollectionNode,
10
+ IndexNode,
11
+ UnionNode,
12
+ MembershipTestNode,
13
+ TypeCastNode,
14
+ TypeReferenceNode,
15
+ TypeOrIdentifierNode
16
+ } from '../parser/ast';
17
+ import { NodeType } from '../parser/ast';
18
+ import { TokenType } from '../lexer/token';
19
+ import type { EvaluationResult } from './types';
20
+ import type { RuntimeContext } from '../runtime/context';
21
+ import { EvaluationError, CollectionUtils } from './types';
22
+ import { RuntimeContextManager } from '../runtime/context';
23
+ import { TypeSystem } from '../registry/utils/type-system';
24
+ import { Registry } from '../registry';
25
+ import type { Interpreter as IInterpreter } from '../registry/types';
26
+
27
+ // Import registry to trigger operation registration
28
+ import '../registry';
29
+
30
+ // Type for node evaluator functions
31
+ type NodeEvaluator = (node: any, input: any[], context: RuntimeContext) => EvaluationResult;
32
+
33
+ /**
34
+ * FHIRPath Interpreter - evaluates AST nodes following the stream-processing model.
35
+ * Every node is a processing unit: (input, context) → (output, new context)
36
+ *
37
+ * This refactored version uses object lookup instead of switch statements.
38
+ */
39
+ export class Interpreter implements IInterpreter {
40
+ // Object lookup for node evaluators
41
+ private readonly nodeEvaluators: Record<NodeType, NodeEvaluator> = {
42
+ [NodeType.Literal]: this.evaluateLiteral.bind(this),
43
+ [NodeType.Identifier]: this.evaluateIdentifier.bind(this),
44
+ [NodeType.TypeOrIdentifier]: this.evaluateTypeOrIdentifier.bind(this),
45
+ [NodeType.Variable]: this.evaluateVariable.bind(this),
46
+ [NodeType.Binary]: this.evaluateBinary.bind(this),
47
+ [NodeType.Unary]: this.evaluateUnary.bind(this),
48
+ [NodeType.Function]: this.evaluateFunction.bind(this),
49
+ [NodeType.Collection]: this.evaluateCollection.bind(this),
50
+ [NodeType.Index]: this.evaluateIndex.bind(this),
51
+ [NodeType.Union]: this.evaluateUnion.bind(this),
52
+ [NodeType.MembershipTest]: this.evaluateMembershipTest.bind(this),
53
+ [NodeType.TypeCast]: this.evaluateTypeCast.bind(this),
54
+ [NodeType.TypeReference]: this.evaluateTypeReference.bind(this),
55
+ [NodeType.Error]: this.evaluateError.bind(this),
56
+ [NodeType.Incomplete]: this.evaluateIncomplete.bind(this),
57
+ };
58
+
59
+ /**
60
+ * Main evaluation method - uses object lookup instead of switch
61
+ */
62
+ evaluate(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
63
+ try {
64
+ // Ensure $this is set in the context if not already present
65
+ if (!RuntimeContextManager.getVariable(context, '$this')) {
66
+ context = RuntimeContextManager.setSpecialVariable(context, 'this', input);
67
+ }
68
+
69
+ const evaluator = this.nodeEvaluators[node.type];
70
+
71
+ if (!evaluator) {
72
+ throw new EvaluationError(
73
+ `Unknown node type: ${node.type}`,
74
+ node.position
75
+ );
76
+ }
77
+
78
+ return evaluator(node, input, context);
79
+ } catch (error) {
80
+ // Add position information if not already present
81
+ if (error instanceof EvaluationError && !error.position && node.position) {
82
+ error.position = node.position;
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ private evaluateLiteral(node: LiteralNode, input: any[], context: RuntimeContext): EvaluationResult {
89
+ // If literal has operation reference from parser
90
+ if (node.operation && node.operation.kind === 'literal') {
91
+ return node.operation.evaluate(this, context, input);
92
+ }
93
+
94
+ // Fallback for legacy literals
95
+ const value = node.value === null ? [] : [node.value];
96
+ return { value, context };
97
+ }
98
+
99
+ private evaluateIdentifier(node: IdentifierNode, input: any[], context: RuntimeContext): EvaluationResult {
100
+ // Check if this identifier could be a resource type name
101
+ // Resource types in FHIR typically start with uppercase
102
+ if (node.name[0] === node?.name?.[0]?.toUpperCase()) {
103
+ // Check if any input items have this as their resourceType
104
+ const hasMatchingResourceType = input.some(item =>
105
+ item && typeof item === 'object' && item.resourceType === node.name
106
+ );
107
+
108
+ if (hasMatchingResourceType) {
109
+ // This is a type filter - return only items matching this resourceType
110
+ const filtered = input.filter(item =>
111
+ item && typeof item === 'object' && item.resourceType === node.name
112
+ );
113
+ return { value: filtered, context };
114
+ }
115
+ }
116
+
117
+ // Regular property navigation
118
+ const results: any[] = [];
119
+
120
+ for (const item of input) {
121
+ if (item == null || typeof item !== 'object') {
122
+ // Primitives don't have properties - skip
123
+ continue;
124
+ }
125
+
126
+ const value = item[node.name];
127
+ if (value !== undefined) {
128
+ // Add to results - flatten if array
129
+ if (Array.isArray(value)) {
130
+ results.push(...value);
131
+ } else {
132
+ results.push(value);
133
+ }
134
+ }
135
+ // Missing properties return empty (not added to results)
136
+ }
137
+
138
+ return { value: results, context };
139
+ }
140
+
141
+ private evaluateTypeOrIdentifier(node: TypeOrIdentifierNode, input: any[], context: RuntimeContext): EvaluationResult {
142
+ // TypeOrIdentifier can act as either a type reference or property navigation
143
+
144
+ // First, check if this is a known type name (e.g., Patient, Observation)
145
+ // In FHIR context, type names match resourceType values
146
+ const possibleTypeName = node.name;
147
+
148
+ // Check if any input items have this as their resourceType
149
+ const hasMatchingResourceType = input.some(item =>
150
+ item && typeof item === 'object' && item.resourceType === possibleTypeName
151
+ );
152
+
153
+ if (hasMatchingResourceType) {
154
+ // This is a type filter - return only items matching this resourceType
155
+ const filtered = input.filter(item =>
156
+ item && typeof item === 'object' && item.resourceType === possibleTypeName
157
+ );
158
+ return { value: filtered, context };
159
+ }
160
+
161
+ // Not a type filter, treat as property navigation
162
+ return this.evaluateIdentifier(node as any, input, context);
163
+ }
164
+
165
+ private evaluateVariable(node: VariableNode, input: any[], context: RuntimeContext): EvaluationResult {
166
+ // Variables ignore input and return value from context
167
+ const value = RuntimeContextManager.getVariable(context, node.name);
168
+
169
+ if (value === undefined) {
170
+ // Special handling for unknown special variables
171
+ if (node.name.startsWith('$') && !['$this', '$index', '$total'].includes(node.name)) {
172
+ throw new EvaluationError(`Unknown special variable: ${node.name}`, node.position);
173
+ }
174
+ return { value: [], context };
175
+ }
176
+
177
+ // Ensure we always return an array
178
+ const arrayValue = Array.isArray(value) ? value : [value];
179
+ return { value: arrayValue, context };
180
+ }
181
+
182
+ private evaluateBinary(node: BinaryNode, input: any[], context: RuntimeContext): EvaluationResult {
183
+ // Special handling for dot operator - it's a pipeline
184
+ if (node.operator === TokenType.DOT) {
185
+ // Phase 1: Evaluate left with original input/context
186
+ const leftResult = this.evaluate(node.left, input, context);
187
+
188
+ // Phase 2: Evaluate right with left's output as input
189
+ const rightResult = this.evaluate(node.right, leftResult.value, leftResult.context);
190
+
191
+ return rightResult;
192
+ }
193
+
194
+ // Handle case where parser incorrectly creates BinaryNode for unary minus
195
+ if (!node.left && !node.right && (node as any).operand) {
196
+ // This is actually a unary operation
197
+ const unaryOp = Registry.getByToken(node.operator, 'prefix');
198
+ if (unaryOp && unaryOp.kind === 'operator') {
199
+ const operandResult = this.evaluate((node as any).operand, input, context);
200
+ return unaryOp.evaluate(this, operandResult.context, input, operandResult.value);
201
+ }
202
+ }
203
+
204
+ // Get operation from registry (binary operators are infix)
205
+ const operation = node.operation || Registry.getByToken(node.operator, 'infix');
206
+ if (!operation || operation.kind !== 'operator') {
207
+ throw new EvaluationError(`Unknown operator: ${node.operator}`, node.position);
208
+ }
209
+
210
+ if (!node.left || !node.right) {
211
+ throw new EvaluationError(`Binary operator ${node.operator} missing operands`, node.position);
212
+ }
213
+
214
+ // Special handling for union operator - both sides should use the same context
215
+ if (node.operator === TokenType.PIPE) {
216
+ const leftResult = this.evaluate(node.left, input, context);
217
+ const rightResult = this.evaluate(node.right, input, context); // Use original context, not leftResult.context
218
+
219
+ // Use operation's evaluate method
220
+ return operation.evaluate(this, context, input, leftResult.value, rightResult.value);
221
+ }
222
+
223
+ // Normal operators - context flows from left to right
224
+ const leftResult = this.evaluate(node.left, input, context);
225
+ const rightResult = this.evaluate(node.right, input, leftResult.context);
226
+
227
+ // Use operation's evaluate method
228
+ return operation.evaluate(this, rightResult.context, input, leftResult.value, rightResult.value);
229
+ }
230
+
231
+ private evaluateUnary(node: UnaryNode, input: any[], context: RuntimeContext): EvaluationResult {
232
+ // Get operation from registry (unary operators are prefix)
233
+ // Don't use node.operation as parser might have assigned wrong operation
234
+ const operation = Registry.getByToken(node.operator, 'prefix');
235
+ if (!operation || operation.kind !== 'operator') {
236
+ throw new EvaluationError(`Unknown unary operator: ${node.operator}`, node.position);
237
+ }
238
+
239
+ // Evaluate operand
240
+ const operandResult = this.evaluate(node.operand, input, context);
241
+
242
+ // Use operation's evaluate method
243
+ return operation.evaluate(this, operandResult.context, input, operandResult.value);
244
+ }
245
+
246
+ private evaluateFunction(node: FunctionNode, input: any[], context: RuntimeContext): EvaluationResult {
247
+ // Extract function name and handle method call syntax
248
+ let funcName: string;
249
+ let functionInput = input;
250
+
251
+ if (node.name.type === NodeType.Identifier) {
252
+ funcName = (node.name as IdentifierNode).name;
253
+ } else if (node.name.type === NodeType.Binary && (node.name as BinaryNode).operator === TokenType.DOT) {
254
+ // Method call syntax: expression.function(args)
255
+ const binaryNode = node.name as BinaryNode;
256
+
257
+ // Evaluate the left side to get the input
258
+ const leftResult = this.evaluate(binaryNode.left, input, context);
259
+ functionInput = leftResult.value;
260
+ context = leftResult.context;
261
+
262
+ // Get the function name from the right side
263
+ if (binaryNode.right.type === NodeType.Identifier) {
264
+ funcName = (binaryNode.right as IdentifierNode).name;
265
+ } else {
266
+ throw new EvaluationError('Invalid method call syntax', node.position);
267
+ }
268
+ } else {
269
+ throw new EvaluationError('Complex function names not yet supported', node.position);
270
+ }
271
+
272
+ // Check for custom functions first
273
+ if ((context as any).customFunctions && funcName in (context as any).customFunctions) {
274
+ const customFunc = (context as any).customFunctions[funcName];
275
+ // Evaluate all arguments
276
+ const evaluatedArgs: any[] = [];
277
+ for (const arg of node.arguments) {
278
+ const argResult = this.evaluate(arg, functionInput, context);
279
+ evaluatedArgs.push(argResult.value);
280
+ context = argResult.context;
281
+ }
282
+
283
+ // Call custom function
284
+ const result = customFunc!(context, functionInput, ...evaluatedArgs);
285
+ return { value: result, context };
286
+ }
287
+
288
+ // Get function from registry
289
+ const operation = Registry.get(funcName);
290
+ if (!operation || operation.kind !== 'function') {
291
+ throw new EvaluationError(`Unknown function: ${funcName}`, node.position);
292
+ }
293
+
294
+ // Check propagateEmptyInput flag
295
+ if (operation.signature.propagatesEmpty && functionInput.length === 0) {
296
+ return { value: [], context };
297
+ }
298
+
299
+ // Evaluate arguments based on parameter definitions
300
+ const evaluatedArgs: any[] = [];
301
+ for (let i = 0; i < node.arguments.length; i++) {
302
+ const arg = node.arguments[i];
303
+ const param = operation.signature.parameters[i];
304
+
305
+ if (param && param.kind === 'expression') {
306
+ // Pass expression as-is, will be evaluated by the function
307
+ evaluatedArgs.push(arg);
308
+ } else {
309
+ // Evaluate the argument to get its value
310
+ const argResult = this.evaluate(arg!, functionInput, context);
311
+ evaluatedArgs.push(argResult.value);
312
+ context = argResult.context;
313
+ }
314
+ }
315
+
316
+ // Use operation's evaluate method
317
+ return operation.evaluate(this, context, functionInput, ...evaluatedArgs);
318
+ }
319
+
320
+ private evaluateCollection(node: CollectionNode, input: any[], context: RuntimeContext): EvaluationResult {
321
+ // Evaluate each element and combine results
322
+ const results: any[] = [];
323
+ let currentContext = context;
324
+
325
+ for (const element of node.elements) {
326
+ const result = this.evaluate(element, input, currentContext);
327
+ results.push(...result.value);
328
+ currentContext = result.context;
329
+ }
330
+
331
+ return { value: results, context: currentContext };
332
+ }
333
+
334
+ private evaluateIndex(node: IndexNode, input: any[], context: RuntimeContext): EvaluationResult {
335
+ // Evaluate the expression being indexed
336
+ const exprResult = this.evaluate(node.expression, input, context);
337
+
338
+ // Evaluate the index expression in the original context
339
+ const indexResult = this.evaluate(node.index, input, context);
340
+
341
+ // Index must be a single integer
342
+ if (indexResult.value.length === 0) {
343
+ return { value: [], context: indexResult.context };
344
+ }
345
+
346
+ const index = CollectionUtils.toSingleton(indexResult.value);
347
+ if (typeof index !== 'number' || !Number.isInteger(index)) {
348
+ throw new EvaluationError('Index must be an integer', node.position);
349
+ }
350
+
351
+ // FHIRPath uses 0-based indexing
352
+ if (index < 0 || index >= exprResult.value.length) {
353
+ // Out of bounds returns empty
354
+ return { value: [], context: indexResult.context };
355
+ }
356
+
357
+ return { value: [exprResult.value[index]], context: indexResult.context };
358
+ }
359
+
360
+ private evaluateUnion(node: UnionNode, input: any[], context: RuntimeContext): EvaluationResult {
361
+ // Union combines results from all operands
362
+ // Each operand should be evaluated with the SAME original context
363
+ // to prevent variable definitions from leaking between branches
364
+ const results: any[] = [];
365
+ const seen = new Set();
366
+
367
+ for (const operand of node.operands) {
368
+ // Always use the original context for each operand
369
+ const result = this.evaluate(operand, input, context);
370
+
371
+ // Remove duplicates
372
+ for (const item of result.value) {
373
+ const key = JSON.stringify(item);
374
+ if (!seen.has(key)) {
375
+ seen.add(key);
376
+ results.push(item);
377
+ }
378
+ }
379
+ }
380
+
381
+ // Return the original context, not a modified one
382
+ return { value: results, context };
383
+ }
384
+
385
+ private evaluateMembershipTest(node: MembershipTestNode, input: any[], context: RuntimeContext): EvaluationResult {
386
+ // Evaluate the expression to get values to test
387
+ const exprResult = this.evaluate(node.expression, input, context);
388
+
389
+ // Empty collection: is returns empty
390
+ if (exprResult.value.length === 0) {
391
+ return { value: [], context: exprResult.context };
392
+ }
393
+
394
+ // Check if ALL values match the type
395
+ for (const value of exprResult.value) {
396
+ if (!TypeSystem.isType(value, node.targetType)) {
397
+ return { value: [false], context: exprResult.context };
398
+ }
399
+ }
400
+
401
+ // All values match the type
402
+ return { value: [true], context: exprResult.context };
403
+ }
404
+
405
+ private evaluateTypeCast(node: TypeCastNode, input: any[], context: RuntimeContext): EvaluationResult {
406
+ // Evaluate the expression to get values to cast
407
+ const exprResult = this.evaluate(node.expression, input, context);
408
+
409
+ // For each value, attempt to cast to the target type
410
+ const results: any[] = [];
411
+ for (const value of exprResult.value) {
412
+ // If already the correct type, keep it
413
+ if (TypeSystem.isType(value, node.targetType)) {
414
+ results.push(value);
415
+ }
416
+ // Otherwise, try to cast (returns null if fails)
417
+ else {
418
+ const castValue = TypeSystem.cast(value, node.targetType);
419
+ if (castValue !== null) {
420
+ results.push(castValue);
421
+ }
422
+ // Failed casts are filtered out (not added to results)
423
+ }
424
+ }
425
+
426
+ // Return filtered collection
427
+ return { value: results, context: exprResult.context };
428
+ }
429
+
430
+ private evaluateTypeReference(node: TypeReferenceNode, input: any[], context: RuntimeContext): EvaluationResult {
431
+ // Type references don't evaluate to values directly
432
+ throw new EvaluationError(`Type reference cannot be evaluated: ${node.typeName}`, node.position);
433
+ }
434
+
435
+ private evaluateError(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
436
+ // Error nodes evaluate to empty collection
437
+ return { value: [], context };
438
+ }
439
+
440
+ private evaluateIncomplete(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
441
+ // Incomplete nodes evaluate to empty collection
442
+ return { value: [], context };
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Helper function to evaluate a FHIRPath expression
448
+ */
449
+ export function evaluateFHIRPath(
450
+ expression: string | ASTNode,
451
+ input: any,
452
+ context?: RuntimeContext
453
+ ): any[] {
454
+ // Parse if string
455
+ const ast = typeof expression === 'string'
456
+ ? require('../parser').parse(expression)
457
+ : expression;
458
+
459
+ // Convert input to collection
460
+ const inputCollection = CollectionUtils.toCollection(input);
461
+
462
+ // Create context if not provided
463
+ let evalContext = context || RuntimeContextManager.create(inputCollection);
464
+
465
+ // Set initial $this to the input collection if not already set
466
+ if (!RuntimeContextManager.getVariable(evalContext, '$this')) {
467
+ evalContext = RuntimeContextManager.setSpecialVariable(evalContext, 'this', inputCollection);
468
+ }
469
+
470
+ // Create interpreter and evaluate
471
+ const interpreter = new Interpreter();
472
+ const result = interpreter.evaluate(ast, inputCollection, evalContext);
473
+
474
+ return result.value;
475
+ }