@atomic-ehr/fhirpath 0.0.1-canary.1825db0.20250725140030

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