@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800

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