@atomic-ehr/fhirpath 0.0.1-canary.8687028.20250724113707

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 (54) hide show
  1. package/README.md +307 -0
  2. package/dist/index.js +7975 -0
  3. package/package.json +48 -0
  4. package/src/analyzer/analyzer.ts +486 -0
  5. package/src/analyzer/model-provider.ts +244 -0
  6. package/src/analyzer/schemas/index.ts +2 -0
  7. package/src/analyzer/schemas/types.ts +40 -0
  8. package/src/analyzer/types.ts +142 -0
  9. package/src/api/builder.ts +148 -0
  10. package/src/api/errors.ts +134 -0
  11. package/src/api/expression.ts +149 -0
  12. package/src/api/index.ts +57 -0
  13. package/src/api/registry.ts +128 -0
  14. package/src/api/types.ts +154 -0
  15. package/src/compiler/compiler.ts +550 -0
  16. package/src/compiler/index.ts +2 -0
  17. package/src/compiler/types.ts +23 -0
  18. package/src/index.ts +52 -0
  19. package/src/interpreter/README.md +78 -0
  20. package/src/interpreter/context.ts +181 -0
  21. package/src/interpreter/interpreter.ts +429 -0
  22. package/src/interpreter/types.ts +132 -0
  23. package/src/lexer/char-tables.ts +37 -0
  24. package/src/lexer/errors.ts +31 -0
  25. package/src/lexer/index.ts +5 -0
  26. package/src/lexer/lexer.ts +745 -0
  27. package/src/lexer/token.ts +104 -0
  28. package/src/parser/ast.ts +123 -0
  29. package/src/parser/index.ts +3 -0
  30. package/src/parser/parser.ts +701 -0
  31. package/src/parser/pprint.ts +169 -0
  32. package/src/registry/default-analyzers.ts +257 -0
  33. package/src/registry/default-compilers.ts +31 -0
  34. package/src/registry/index.ts +93 -0
  35. package/src/registry/operations/arithmetic.ts +506 -0
  36. package/src/registry/operations/collection.ts +384 -0
  37. package/src/registry/operations/comparison.ts +432 -0
  38. package/src/registry/operations/existence.ts +719 -0
  39. package/src/registry/operations/filtering.ts +374 -0
  40. package/src/registry/operations/literals.ts +341 -0
  41. package/src/registry/operations/logical.ts +402 -0
  42. package/src/registry/operations/math.ts +128 -0
  43. package/src/registry/operations/membership.ts +132 -0
  44. package/src/registry/operations/string.ts +507 -0
  45. package/src/registry/operations/subsetting.ts +174 -0
  46. package/src/registry/operations/type-checking.ts +162 -0
  47. package/src/registry/operations/type-conversion.ts +404 -0
  48. package/src/registry/operations/type-operators.ts +307 -0
  49. package/src/registry/operations/utility.ts +510 -0
  50. package/src/registry/registry.ts +146 -0
  51. package/src/registry/types.ts +162 -0
  52. package/src/registry/utils/evaluation-helpers.ts +93 -0
  53. package/src/registry/utils/index.ts +3 -0
  54. package/src/registry/utils/type-system.ts +173 -0
@@ -0,0 +1,181 @@
1
+ import type { Context } from './types';
2
+
3
+ /**
4
+ * Context management utilities for FHIRPath interpreter.
5
+ * Context flows parallel to data through expressions.
6
+ *
7
+ * Uses JavaScript prototype chain for efficient inheritance.
8
+ */
9
+ export class ContextManager {
10
+ /**
11
+ * Create a new empty context
12
+ */
13
+ static create(initialInput?: any[]): Context {
14
+ // Create base context with null prototype to avoid Object.prototype pollution
15
+ const context = Object.create(null) as Context;
16
+
17
+ // Initialize with prototype-based objects
18
+ context.variables = Object.create(null);
19
+ context.env = Object.create(null);
20
+
21
+ // Set root variables
22
+ context.$context = initialInput;
23
+ context.$resource = initialInput;
24
+ context.$rootResource = initialInput;
25
+
26
+ return context;
27
+ }
28
+
29
+ /**
30
+ * Create a child context inheriting from parent via prototype chain
31
+ * O(1) operation - no copying needed
32
+ */
33
+ static copy(context: Context): Context {
34
+ // Create child context with parent as prototype
35
+ const newContext = Object.create(context) as Context;
36
+
37
+ // Create child objects that inherit from parent's objects
38
+ newContext.variables = Object.create(context.variables);
39
+ newContext.env = Object.create(context.env);
40
+
41
+ // Root variables are inherited automatically through prototype chain
42
+ // No need to copy them unless they change
43
+
44
+ return newContext;
45
+ }
46
+
47
+ /**
48
+ * Add or update a variable in context
49
+ * Only sets on current context level, shadowing parent values
50
+ */
51
+ static setVariable(context: Context, name: string, value: any[]): Context {
52
+ const newContext = this.copy(context);
53
+ newContext.variables[name] = value;
54
+ return newContext;
55
+ }
56
+
57
+ /**
58
+ * Get a variable value from context
59
+ * Uses prototype chain for lookup
60
+ */
61
+ static getVariable(context: Context, name: string): any[] | undefined {
62
+ // Check user-defined variables first (prototype chain handles inheritance)
63
+ if (name in context.variables) {
64
+ return context.variables[name];
65
+ }
66
+
67
+ // Check special root variables
68
+ switch (name) {
69
+ case 'context':
70
+ return context.$context;
71
+ case 'resource':
72
+ return context.$resource;
73
+ case 'rootResource':
74
+ return context.$rootResource;
75
+ default:
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Set iterator context ($this, $index) - used by where(), select(), etc.
82
+ */
83
+ static setIteratorContext(
84
+ context: Context,
85
+ item: any,
86
+ index: number
87
+ ): Context {
88
+ const newContext = this.copy(context);
89
+ // Only set changed values - prototype provides the rest
90
+ newContext.env.$this = [item];
91
+ newContext.env.$index = index;
92
+ return newContext;
93
+ }
94
+
95
+ /**
96
+ * Set aggregate context ($total) - used by aggregate()
97
+ */
98
+ static setAggregateContext(
99
+ context: Context,
100
+ total: any[]
101
+ ): Context {
102
+ const newContext = this.copy(context);
103
+ newContext.env.$total = total;
104
+ return newContext;
105
+ }
106
+
107
+ /**
108
+ * Clear iterator/aggregate context - restore to original
109
+ */
110
+ static clearEnv(context: Context): Context {
111
+ const newContext = this.copy(context);
112
+ // Create fresh env object, effectively hiding parent's env values
113
+ newContext.env = Object.create(null);
114
+ return newContext;
115
+ }
116
+
117
+ /**
118
+ * Get special environment variable
119
+ */
120
+ static getEnvVariable(context: Context, name: '$this' | '$index' | '$total'): any {
121
+ switch (name) {
122
+ case '$this':
123
+ return context.env.$this;
124
+ case '$index':
125
+ return context.env.$index;
126
+ case '$total':
127
+ return context.env.$total;
128
+ default:
129
+ return undefined;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Check if a variable exists in context (including inherited)
135
+ */
136
+ static hasVariable(context: Context, name: string): boolean {
137
+ return (name in context.variables) ||
138
+ ['context', 'resource', 'rootResource'].includes(name);
139
+ }
140
+
141
+ /**
142
+ * Debug helper - get all variables including inherited ones
143
+ * Traverses prototype chain to collect all variables
144
+ */
145
+ static getAllVariables(context: Context): Record<string, any[]> {
146
+ const result: Record<string, any[]> = {};
147
+
148
+ // Traverse prototype chain for user variables
149
+ let currentVars = context.variables;
150
+ while (currentVars) {
151
+ for (const key in currentVars) {
152
+ if (!(key in result) && Object.prototype.hasOwnProperty.call(currentVars, key)) {
153
+ result[`%${key}`] = currentVars[key]!;
154
+ }
155
+ }
156
+ currentVars = Object.getPrototypeOf(currentVars);
157
+ }
158
+
159
+ // Root variables
160
+ if (context.$context) result['%context'] = context.$context;
161
+ if (context.$resource) result['%resource'] = context.$resource;
162
+ if (context.$rootResource) result['%rootResource'] = context.$rootResource;
163
+
164
+ // Environment variables (also traverse prototype chain)
165
+ let currentEnv = context.env;
166
+ while (currentEnv) {
167
+ if (currentEnv.$this !== undefined && !('$this' in result)) {
168
+ result['$this'] = currentEnv.$this;
169
+ }
170
+ if (currentEnv.$index !== undefined && !('$index' in result)) {
171
+ result['$index'] = [currentEnv.$index];
172
+ }
173
+ if (currentEnv.$total !== undefined && !('$total' in result)) {
174
+ result['$total'] = currentEnv.$total;
175
+ }
176
+ currentEnv = Object.getPrototypeOf(currentEnv);
177
+ }
178
+
179
+ return result;
180
+ }
181
+ }
@@ -0,0 +1,429 @@
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, EvaluationResult } from './types';
20
+ import { EvaluationError, CollectionUtils } from './types';
21
+ import { ContextManager } from './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: Context) => 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: Context): 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: Context): 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: Context): EvaluationResult {
103
+ // Identifier performs property navigation on each item in input
104
+ const results: any[] = [];
105
+
106
+ for (const item of input) {
107
+ if (item == null || typeof item !== 'object') {
108
+ // Primitives don't have properties - skip
109
+ continue;
110
+ }
111
+
112
+ const value = item[node.name];
113
+ if (value !== undefined) {
114
+ // Add to results - flatten if array
115
+ if (Array.isArray(value)) {
116
+ results.push(...value);
117
+ } else {
118
+ results.push(value);
119
+ }
120
+ }
121
+ // Missing properties return empty (not added to results)
122
+ }
123
+
124
+ return { value: results, context };
125
+ }
126
+
127
+ private evaluateTypeOrIdentifier(node: TypeOrIdentifierNode, input: any[], context: Context): EvaluationResult {
128
+ // TypeOrIdentifier can act as either a type reference or property navigation
129
+ // For now, treat it as an identifier
130
+ return this.evaluateIdentifier(node as any, input, context);
131
+ }
132
+
133
+ private evaluateVariable(node: VariableNode, input: any[], context: Context): EvaluationResult {
134
+ // Variables ignore input and return value from context
135
+ let value: any[] = [];
136
+
137
+ if (node.name.startsWith('$')) {
138
+ // Special environment variables - use object lookup
139
+ const envVarHandlers: Record<string, () => any[]> = {
140
+ '$this': () => context.env.$this || [],
141
+ '$index': () => context.env.$index !== undefined ? [context.env.$index] : [],
142
+ '$total': () => context.env.$total || [],
143
+ };
144
+
145
+ const handler = envVarHandlers[node.name];
146
+ if (!handler) {
147
+ throw new EvaluationError(`Unknown special variable: ${node.name}`, node.position);
148
+ }
149
+ value = handler();
150
+ } else {
151
+ // User-defined variables (remove % prefix if present)
152
+ const varName = node.name.startsWith('%') ? node.name.substring(1) : node.name;
153
+ value = ContextManager.getVariable(context, varName) || [];
154
+ }
155
+
156
+ return { value, context };
157
+ }
158
+
159
+ private evaluateBinary(node: BinaryNode, input: any[], context: Context): EvaluationResult {
160
+ // Special handling for dot operator - it's a pipeline
161
+ if (node.operator === TokenType.DOT) {
162
+ // Phase 1: Evaluate left with original input/context
163
+ const leftResult = this.evaluate(node.left, input, context);
164
+
165
+ // Phase 2: Evaluate right with left's output as input
166
+ const rightResult = this.evaluate(node.right, leftResult.value, leftResult.context);
167
+
168
+ return rightResult;
169
+ }
170
+
171
+ // Handle case where parser incorrectly creates BinaryNode for unary minus
172
+ if (!node.left && !node.right && (node as any).operand) {
173
+ // This is actually a unary operation
174
+ const unaryOp = Registry.getByToken(node.operator, 'prefix');
175
+ if (unaryOp && unaryOp.kind === 'operator') {
176
+ const operandResult = this.evaluate((node as any).operand, input, context);
177
+ return unaryOp.evaluate(this, operandResult.context, input, operandResult.value);
178
+ }
179
+ }
180
+
181
+ // Get operation from registry (binary operators are infix)
182
+ const operation = node.operation || Registry.getByToken(node.operator, 'infix');
183
+ if (!operation || operation.kind !== 'operator') {
184
+ throw new EvaluationError(`Unknown operator: ${node.operator}`, node.position);
185
+ }
186
+
187
+ if (!node.left || !node.right) {
188
+ throw new EvaluationError(`Binary operator ${node.operator} missing operands`, node.position);
189
+ }
190
+
191
+ const leftResult = this.evaluate(node.left, input, context);
192
+ const rightResult = this.evaluate(node.right, input, leftResult.context);
193
+
194
+ // Use operation's evaluate method
195
+ return operation.evaluate(this, rightResult.context, input, leftResult.value, rightResult.value);
196
+ }
197
+
198
+ private evaluateUnary(node: UnaryNode, input: any[], context: Context): EvaluationResult {
199
+ // Get operation from registry (unary operators are prefix)
200
+ // Don't use node.operation as parser might have assigned wrong operation
201
+ const operation = Registry.getByToken(node.operator, 'prefix');
202
+ if (!operation || operation.kind !== 'operator') {
203
+ throw new EvaluationError(`Unknown unary operator: ${node.operator}`, node.position);
204
+ }
205
+
206
+ // Evaluate operand
207
+ const operandResult = this.evaluate(node.operand, input, context);
208
+
209
+ // Use operation's evaluate method
210
+ return operation.evaluate(this, operandResult.context, input, operandResult.value);
211
+ }
212
+
213
+ private evaluateFunction(node: FunctionNode, input: any[], context: Context): EvaluationResult {
214
+ // Extract function name and handle method call syntax
215
+ let funcName: string;
216
+ let functionInput = input;
217
+
218
+ if (node.name.type === NodeType.Identifier) {
219
+ funcName = (node.name as IdentifierNode).name;
220
+ } else if (node.name.type === NodeType.Binary && (node.name as BinaryNode).operator === TokenType.DOT) {
221
+ // Method call syntax: expression.function(args)
222
+ const binaryNode = node.name as BinaryNode;
223
+
224
+ // Evaluate the left side to get the input
225
+ const leftResult = this.evaluate(binaryNode.left, input, context);
226
+ functionInput = leftResult.value;
227
+ context = leftResult.context;
228
+
229
+ // Get the function name from the right side
230
+ if (binaryNode.right.type === NodeType.Identifier) {
231
+ funcName = (binaryNode.right as IdentifierNode).name;
232
+ } else {
233
+ throw new EvaluationError('Invalid method call syntax', node.position);
234
+ }
235
+ } else {
236
+ throw new EvaluationError('Complex function names not yet supported', node.position);
237
+ }
238
+
239
+ // Check for custom functions first
240
+ if (context.customFunctions && funcName in context.customFunctions) {
241
+ const customFunc = context.customFunctions[funcName];
242
+
243
+ // Evaluate all arguments
244
+ const evaluatedArgs: any[] = [];
245
+ for (const arg of node.arguments) {
246
+ const argResult = this.evaluate(arg, functionInput, context);
247
+ evaluatedArgs.push(argResult.value);
248
+ context = argResult.context;
249
+ }
250
+
251
+ // Call custom function
252
+ const result = customFunc!(context, functionInput, ...evaluatedArgs);
253
+ return { value: result, context };
254
+ }
255
+
256
+ // Get function from registry
257
+ const operation = Registry.get(funcName);
258
+ if (!operation || operation.kind !== 'function') {
259
+ throw new EvaluationError(`Unknown function: ${funcName}`, node.position);
260
+ }
261
+
262
+ // Check propagateEmptyInput flag
263
+ if (operation.signature.propagatesEmpty && functionInput.length === 0) {
264
+ return { value: [], context };
265
+ }
266
+
267
+ // Evaluate arguments based on parameter definitions
268
+ const evaluatedArgs: any[] = [];
269
+ for (let i = 0; i < node.arguments.length; i++) {
270
+ const arg = node.arguments[i];
271
+ const param = operation.signature.parameters[i];
272
+
273
+ if (param && param.kind === 'expression') {
274
+ // Pass expression as-is, will be evaluated by the function
275
+ evaluatedArgs.push(arg);
276
+ } else {
277
+ // Evaluate the argument to get its value
278
+ const argResult = this.evaluate(arg!, functionInput, context);
279
+ evaluatedArgs.push(argResult.value);
280
+ context = argResult.context;
281
+ }
282
+ }
283
+
284
+ // Use operation's evaluate method
285
+ return operation.evaluate(this, context, functionInput, ...evaluatedArgs);
286
+ }
287
+
288
+ private evaluateCollection(node: CollectionNode, input: any[], context: Context): EvaluationResult {
289
+ // Evaluate each element and combine results
290
+ const results: any[] = [];
291
+ let currentContext = context;
292
+
293
+ for (const element of node.elements) {
294
+ const result = this.evaluate(element, input, currentContext);
295
+ results.push(...result.value);
296
+ currentContext = result.context;
297
+ }
298
+
299
+ return { value: results, context: currentContext };
300
+ }
301
+
302
+ private evaluateIndex(node: IndexNode, input: any[], context: Context): EvaluationResult {
303
+ // Evaluate the expression being indexed
304
+ const exprResult = this.evaluate(node.expression, input, context);
305
+
306
+ // Evaluate the index expression in the original context
307
+ const indexResult = this.evaluate(node.index, input, context);
308
+
309
+ // Index must be a single integer
310
+ if (indexResult.value.length === 0) {
311
+ return { value: [], context: indexResult.context };
312
+ }
313
+
314
+ const index = CollectionUtils.toSingleton(indexResult.value);
315
+ if (typeof index !== 'number' || !Number.isInteger(index)) {
316
+ throw new EvaluationError('Index must be an integer', node.position);
317
+ }
318
+
319
+ // FHIRPath uses 0-based indexing
320
+ if (index < 0 || index >= exprResult.value.length) {
321
+ // Out of bounds returns empty
322
+ return { value: [], context: indexResult.context };
323
+ }
324
+
325
+ return { value: [exprResult.value[index]], context: indexResult.context };
326
+ }
327
+
328
+ private evaluateUnion(node: UnionNode, input: any[], context: Context): EvaluationResult {
329
+ // Union combines results from all operands
330
+ const results: any[] = [];
331
+ let currentContext = context;
332
+
333
+ for (const operand of node.operands) {
334
+ const result = this.evaluate(operand, input, currentContext);
335
+ results.push(...result.value);
336
+ // Thread context through operands
337
+ currentContext = result.context;
338
+ }
339
+
340
+ return { value: results, context: currentContext };
341
+ }
342
+
343
+ private evaluateMembershipTest(node: MembershipTestNode, input: any[], context: Context): EvaluationResult {
344
+ // Evaluate the expression to get values to test
345
+ const exprResult = this.evaluate(node.expression, input, context);
346
+
347
+ // Empty collection: is returns empty
348
+ if (exprResult.value.length === 0) {
349
+ return { value: [], context: exprResult.context };
350
+ }
351
+
352
+ // Check if ALL values match the type
353
+ for (const value of exprResult.value) {
354
+ if (!TypeSystem.isType(value, node.targetType)) {
355
+ return { value: [false], context: exprResult.context };
356
+ }
357
+ }
358
+
359
+ // All values match the type
360
+ return { value: [true], context: exprResult.context };
361
+ }
362
+
363
+ private evaluateTypeCast(node: TypeCastNode, input: any[], context: Context): EvaluationResult {
364
+ // Evaluate the expression to get values to cast
365
+ const exprResult = this.evaluate(node.expression, input, context);
366
+
367
+ // For each value, attempt to cast to the target type
368
+ const results: any[] = [];
369
+ for (const value of exprResult.value) {
370
+ // If already the correct type, keep it
371
+ if (TypeSystem.isType(value, node.targetType)) {
372
+ results.push(value);
373
+ }
374
+ // Otherwise, try to cast (returns null if fails)
375
+ else {
376
+ const castValue = TypeSystem.cast(value, node.targetType);
377
+ if (castValue !== null) {
378
+ results.push(castValue);
379
+ }
380
+ // Failed casts are filtered out (not added to results)
381
+ }
382
+ }
383
+
384
+ // Return filtered collection
385
+ return { value: results, context: exprResult.context };
386
+ }
387
+
388
+ private evaluateTypeReference(node: TypeReferenceNode, input: any[], context: Context): EvaluationResult {
389
+ // Type references don't evaluate to values directly
390
+ throw new EvaluationError(`Type reference cannot be evaluated: ${node.typeName}`, node.position);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Helper function to evaluate a FHIRPath expression
396
+ */
397
+ export function evaluateFHIRPath(
398
+ expression: string | ASTNode,
399
+ input: any,
400
+ context?: Context
401
+ ): any[] {
402
+ // Parse if string
403
+ const ast = typeof expression === 'string'
404
+ ? require('../parser').parse(expression)
405
+ : expression;
406
+
407
+ // Convert input to collection
408
+ const inputCollection = CollectionUtils.toCollection(input);
409
+
410
+ // Create context if not provided and set initial $this
411
+ let evalContext = context || ContextManager.create(inputCollection);
412
+
413
+ // Set initial $this to the input collection if not already set
414
+ if (!evalContext.env.$this) {
415
+ evalContext = {
416
+ ...evalContext,
417
+ env: {
418
+ ...evalContext.env,
419
+ $this: inputCollection
420
+ }
421
+ };
422
+ }
423
+
424
+ // Create interpreter and evaluate
425
+ const interpreter = new Interpreter();
426
+ const result = interpreter.evaluate(ast, inputCollection, evalContext);
427
+
428
+ return result.value;
429
+ }