@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.
- package/README.md +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8185 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +152 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
- 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
|
+
}
|