@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
|
@@ -0,0 +1,579 @@
|
|
|
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 { CompiledNode } from './types';
|
|
20
|
+
import type { EvaluationResult } from '../interpreter/types';
|
|
21
|
+
import { EvaluationError, CollectionUtils } from '../interpreter/types';
|
|
22
|
+
import { RuntimeContextManager } from '../runtime/context';
|
|
23
|
+
import { isTruthy, toSingleton } from '../registry/utils';
|
|
24
|
+
import type { Compiler as ICompiler, CompiledExpression, TypeRef } from '../registry/types';
|
|
25
|
+
import type { RuntimeContext } from '../runtime/context';
|
|
26
|
+
// Import the global registry to ensure all operations are registered
|
|
27
|
+
import '../registry';
|
|
28
|
+
import { Registry } from '../registry';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* FHIRPath to JavaScript Closure Compiler
|
|
32
|
+
*
|
|
33
|
+
* Transforms FHIRPath AST nodes into JavaScript functions that implement
|
|
34
|
+
* the same stream-processing semantics as the interpreter.
|
|
35
|
+
*/
|
|
36
|
+
export class Compiler implements ICompiler {
|
|
37
|
+
/**
|
|
38
|
+
* Main entry point - compiles an AST into an executable function
|
|
39
|
+
*/
|
|
40
|
+
compile(node: ASTNode, input?: CompiledExpression): CompiledExpression {
|
|
41
|
+
const compiled = this.compileNode(node);
|
|
42
|
+
|
|
43
|
+
// Wrap the compiled function to ensure $this is set
|
|
44
|
+
return {
|
|
45
|
+
...compiled,
|
|
46
|
+
fn: (ctx: RuntimeContext) => {
|
|
47
|
+
// Ensure $this is set if not already present
|
|
48
|
+
if (!ctx.env?.$this) {
|
|
49
|
+
ctx = {
|
|
50
|
+
...ctx,
|
|
51
|
+
env: {
|
|
52
|
+
...ctx.env,
|
|
53
|
+
$this: ctx.input
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return compiled.fn(ctx);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a type name to a TypeRef
|
|
64
|
+
*/
|
|
65
|
+
resolveType(typeName: string): TypeRef {
|
|
66
|
+
// For now, return a simple type reference
|
|
67
|
+
// In the future, this should use a model provider
|
|
68
|
+
return { type: typeName } as TypeRef;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Dispatches to specific compilation methods based on node type
|
|
73
|
+
*/
|
|
74
|
+
private compileNode(node: ASTNode): CompiledExpression {
|
|
75
|
+
switch (node.type) {
|
|
76
|
+
case NodeType.Literal:
|
|
77
|
+
return this.compileLiteral(node as LiteralNode);
|
|
78
|
+
case NodeType.Identifier:
|
|
79
|
+
return this.compileIdentifier(node as IdentifierNode);
|
|
80
|
+
case NodeType.TypeOrIdentifier:
|
|
81
|
+
return this.compileTypeOrIdentifier(node as TypeOrIdentifierNode);
|
|
82
|
+
case NodeType.Variable:
|
|
83
|
+
return this.compileVariable(node as VariableNode);
|
|
84
|
+
case NodeType.Binary:
|
|
85
|
+
return this.compileBinary(node as BinaryNode);
|
|
86
|
+
case NodeType.Unary:
|
|
87
|
+
return this.compileUnary(node as UnaryNode);
|
|
88
|
+
case NodeType.Function:
|
|
89
|
+
return this.compileFunction(node as FunctionNode);
|
|
90
|
+
case NodeType.Collection:
|
|
91
|
+
return this.compileCollection(node as CollectionNode);
|
|
92
|
+
case NodeType.Index:
|
|
93
|
+
return this.compileIndex(node as IndexNode);
|
|
94
|
+
case NodeType.Union:
|
|
95
|
+
return this.compileUnion(node as UnionNode);
|
|
96
|
+
case NodeType.MembershipTest:
|
|
97
|
+
return this.compileMembershipTest(node as MembershipTestNode);
|
|
98
|
+
case NodeType.TypeCast:
|
|
99
|
+
return this.compileTypeCast(node as TypeCastNode);
|
|
100
|
+
case NodeType.TypeReference:
|
|
101
|
+
return this.compileTypeReference(node as TypeReferenceNode);
|
|
102
|
+
default:
|
|
103
|
+
throw new EvaluationError(
|
|
104
|
+
`Unknown node type: ${(node as any).type}`,
|
|
105
|
+
node.position
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compiles a literal node - returns a constant value
|
|
112
|
+
*/
|
|
113
|
+
private compileLiteral(node: LiteralNode): CompiledExpression {
|
|
114
|
+
const value = node.value;
|
|
115
|
+
|
|
116
|
+
// Check if literal is an operation reference
|
|
117
|
+
if (typeof value === 'string') {
|
|
118
|
+
const operation = Registry.get(value);
|
|
119
|
+
if (operation && operation.kind === 'literal') {
|
|
120
|
+
return operation.compile(this, { fn: () => [], type: this.resolveType('Any'), isSingleton: false }, []);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Return a compiled expression for the literal value
|
|
125
|
+
return {
|
|
126
|
+
fn: (ctx: RuntimeContext) => value === null ? [] : [value],
|
|
127
|
+
type: this.resolveType(this.getLiteralType(value)),
|
|
128
|
+
isSingleton: true,
|
|
129
|
+
source: JSON.stringify(value)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getLiteralType(value: any): string {
|
|
134
|
+
if (value === null || value === undefined) return 'Any';
|
|
135
|
+
if (typeof value === 'boolean') return 'Boolean';
|
|
136
|
+
if (typeof value === 'string') return 'String';
|
|
137
|
+
if (typeof value === 'number') {
|
|
138
|
+
return Number.isInteger(value) ? 'Integer' : 'Decimal';
|
|
139
|
+
}
|
|
140
|
+
return 'Any';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compiles an identifier node - performs property navigation
|
|
145
|
+
*/
|
|
146
|
+
private compileIdentifier(node: IdentifierNode): CompiledExpression {
|
|
147
|
+
const name = node.name;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
fn: (ctx: RuntimeContext) => {
|
|
151
|
+
const input = ctx.focus || ctx.input || [];
|
|
152
|
+
const results: any[] = [];
|
|
153
|
+
|
|
154
|
+
for (const item of input) {
|
|
155
|
+
if (item == null || typeof item !== 'object') {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const value = item[name];
|
|
160
|
+
if (value !== undefined) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
results.push(...value);
|
|
163
|
+
} else {
|
|
164
|
+
results.push(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return results;
|
|
170
|
+
},
|
|
171
|
+
type: this.resolveType('Any'), // Would need type inference in a real implementation
|
|
172
|
+
isSingleton: false,
|
|
173
|
+
source: name
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Compiles a TypeOrIdentifier node - handles both type filtering and property navigation
|
|
179
|
+
*/
|
|
180
|
+
private compileTypeOrIdentifier(node: TypeOrIdentifierNode): CompiledExpression {
|
|
181
|
+
const name = node.name;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
fn: (ctx: RuntimeContext) => {
|
|
185
|
+
const input = ctx.focus || ctx.input || [];
|
|
186
|
+
|
|
187
|
+
// First, check if this is a type filter (e.g., Patient in Patient.name)
|
|
188
|
+
// Check if any input items have this as their resourceType
|
|
189
|
+
const hasMatchingResourceType = input.some((item: any) =>
|
|
190
|
+
item && typeof item === 'object' && item.resourceType === name
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (hasMatchingResourceType) {
|
|
194
|
+
// This is a type filter - return only items matching this resourceType
|
|
195
|
+
return input.filter((item: any) =>
|
|
196
|
+
item && typeof item === 'object' && item.resourceType === name
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Not a type filter, treat as property navigation
|
|
201
|
+
const results: any[] = [];
|
|
202
|
+
|
|
203
|
+
for (const item of input) {
|
|
204
|
+
if (item == null || typeof item !== 'object') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const value = item[name];
|
|
209
|
+
if (value !== undefined) {
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
results.push(...value);
|
|
212
|
+
} else {
|
|
213
|
+
results.push(value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return results;
|
|
219
|
+
},
|
|
220
|
+
type: this.resolveType('Any'),
|
|
221
|
+
isSingleton: false,
|
|
222
|
+
source: name
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Compiles a variable node - looks up value from context
|
|
228
|
+
*/
|
|
229
|
+
private compileVariable(node: VariableNode): CompiledExpression {
|
|
230
|
+
const name = node.name;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
fn: (ctx: RuntimeContext) => {
|
|
234
|
+
if (name.startsWith('$')) {
|
|
235
|
+
// Special environment variables
|
|
236
|
+
switch (name) {
|
|
237
|
+
case '$this':
|
|
238
|
+
return ctx.env.$this || [];
|
|
239
|
+
case '$index':
|
|
240
|
+
return ctx.env.$index !== undefined ? [ctx.env.$index] : [];
|
|
241
|
+
case '$total':
|
|
242
|
+
return ctx.env.$total !== undefined ? [ctx.env.$total] : [];
|
|
243
|
+
default:
|
|
244
|
+
throw new EvaluationError(`Unknown special variable: ${name}`, node.position);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// User-defined variables (remove % prefix if present)
|
|
248
|
+
const varName = name.startsWith('%') ? name.substring(1) : name;
|
|
249
|
+
|
|
250
|
+
// Special root variables
|
|
251
|
+
switch (varName) {
|
|
252
|
+
case 'context':
|
|
253
|
+
return ctx.env.$context || ctx.input || [];
|
|
254
|
+
case 'resource':
|
|
255
|
+
return ctx.env.$resource || ctx.input || [];
|
|
256
|
+
case 'rootResource':
|
|
257
|
+
return ctx.env.$rootResource || ctx.input || [];
|
|
258
|
+
default:
|
|
259
|
+
const value = ctx.env[varName];
|
|
260
|
+
if (value === undefined) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
// Wrap non-array values in an array to create a singleton collection
|
|
264
|
+
return Array.isArray(value) ? value : [value];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
type: this.resolveType('Any'),
|
|
269
|
+
isSingleton: false,
|
|
270
|
+
source: name
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compiles a binary operator node
|
|
276
|
+
*/
|
|
277
|
+
private compileBinary(node: BinaryNode): CompiledExpression {
|
|
278
|
+
const operator = node.operator;
|
|
279
|
+
|
|
280
|
+
// Handle case where parser incorrectly creates BinaryNode for unary minus
|
|
281
|
+
if (!node.left && !node.right && (node as any).operand) {
|
|
282
|
+
// This is actually a unary operation
|
|
283
|
+
return this.compileUnary(node as any);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Special handling for dot operator - it's a pipeline
|
|
287
|
+
if (operator === TokenType.DOT) {
|
|
288
|
+
const left = this.compileNode(node.left);
|
|
289
|
+
const right = this.compileNode(node.right);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
fn: (ctx: RuntimeContext) => {
|
|
293
|
+
// Execute left side with the original context
|
|
294
|
+
const leftResult = left.fn(ctx);
|
|
295
|
+
|
|
296
|
+
// Execute right side with left's result as input
|
|
297
|
+
// Use withInput to maintain prototype chain
|
|
298
|
+
const rightCtx = RuntimeContextManager.withInput(ctx, leftResult);
|
|
299
|
+
return right.fn(rightCtx);
|
|
300
|
+
},
|
|
301
|
+
type: right.type,
|
|
302
|
+
isSingleton: right.isSingleton,
|
|
303
|
+
source: `${left.source || ''}.${right.source || ''}`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Get operation from registry
|
|
308
|
+
const operation = node.operation || Registry.getByToken(operator, 'infix');
|
|
309
|
+
if (!operation || operation.kind !== 'operator') {
|
|
310
|
+
throw new EvaluationError(`Unknown operator: ${operator}`, node.position);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Compile operands
|
|
314
|
+
const left = this.compileNode(node.left);
|
|
315
|
+
const right = this.compileNode(node.right);
|
|
316
|
+
|
|
317
|
+
// Use operation's compile method
|
|
318
|
+
// For operators, pass both operands in args array
|
|
319
|
+
return operation.compile(this, left, [left, right]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compiles a unary operator node
|
|
324
|
+
*/
|
|
325
|
+
private compileUnary(node: UnaryNode): CompiledExpression {
|
|
326
|
+
const operator = node.operator;
|
|
327
|
+
|
|
328
|
+
// Get operation from registry
|
|
329
|
+
// Don't use node.operation as parser might have assigned wrong operation
|
|
330
|
+
const operation = Registry.getByToken(operator, 'prefix');
|
|
331
|
+
if (!operation || operation.kind !== 'operator') {
|
|
332
|
+
throw new EvaluationError(`Unknown unary operator: ${operator}`, node.position);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Compile operand
|
|
336
|
+
const operand = this.compileNode(node.operand);
|
|
337
|
+
|
|
338
|
+
// Use operation's compile method
|
|
339
|
+
// For unary operators, pass operand in args array
|
|
340
|
+
return operation.compile(this, operand, [operand]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Compiles a function call node
|
|
345
|
+
*/
|
|
346
|
+
private compileFunction(node: FunctionNode): CompiledExpression {
|
|
347
|
+
// For now, handle only identifier function names
|
|
348
|
+
if (node.name.type !== NodeType.Identifier) {
|
|
349
|
+
throw new EvaluationError('Dynamic function names not yet supported', node.position);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const functionName = (node.name as IdentifierNode).name;
|
|
353
|
+
|
|
354
|
+
// Check if function is registered
|
|
355
|
+
const operation = Registry.get(functionName);
|
|
356
|
+
if (!operation || operation.kind !== 'function') {
|
|
357
|
+
throw new EvaluationError(`Unknown function: ${functionName}`, node.position);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Compile arguments
|
|
361
|
+
const compiledArgs = node.arguments.map(arg => this.compileNode(arg));
|
|
362
|
+
|
|
363
|
+
// Use operation's compile method
|
|
364
|
+
// For functions, the input is passed as the first compiled expression
|
|
365
|
+
const inputExpr: CompiledExpression = {
|
|
366
|
+
fn: (ctx) => ctx.focus || ctx.input || [],
|
|
367
|
+
type: this.resolveType('Any'),
|
|
368
|
+
isSingleton: false
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return operation.compile(this, inputExpr, compiledArgs);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Compiles a collection node
|
|
376
|
+
*/
|
|
377
|
+
private compileCollection(node: CollectionNode): CompiledExpression {
|
|
378
|
+
const compiledElements = node.elements.map(elem => this.compileNode(elem));
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
fn: (ctx: RuntimeContext) => {
|
|
382
|
+
const results: any[] = [];
|
|
383
|
+
|
|
384
|
+
for (const element of compiledElements) {
|
|
385
|
+
const elementResult = element.fn(ctx);
|
|
386
|
+
results.push(...elementResult);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return results;
|
|
390
|
+
},
|
|
391
|
+
type: this.resolveType('Any'),
|
|
392
|
+
isSingleton: false,
|
|
393
|
+
source: `{${compiledElements.map(e => e.source || '').join(', ')}}`
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Compiles an index node
|
|
399
|
+
*/
|
|
400
|
+
private compileIndex(node: IndexNode): CompiledExpression {
|
|
401
|
+
const expression = this.compileNode(node.expression);
|
|
402
|
+
const index = this.compileNode(node.index);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
fn: (ctx: RuntimeContext) => {
|
|
406
|
+
const exprResult = expression.fn(ctx);
|
|
407
|
+
// Evaluate index in the original context
|
|
408
|
+
const indexResult = index.fn(ctx);
|
|
409
|
+
|
|
410
|
+
if (indexResult.length === 0) {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const idx = toSingleton(indexResult);
|
|
415
|
+
if (typeof idx !== 'number' || !Number.isInteger(idx)) {
|
|
416
|
+
throw new EvaluationError('Index must be an integer', node.position);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (idx < 0 || idx >= exprResult.length) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return [exprResult[idx]];
|
|
424
|
+
},
|
|
425
|
+
type: expression.type,
|
|
426
|
+
isSingleton: true,
|
|
427
|
+
source: `${expression.source || ''}[${index.source || ''}]`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Compiles a union node
|
|
433
|
+
*/
|
|
434
|
+
private compileUnion(node: UnionNode): CompiledExpression {
|
|
435
|
+
const compiledOperands = node.operands.map(op => this.compileNode(op));
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
fn: (ctx: RuntimeContext) => {
|
|
439
|
+
const results: any[] = [];
|
|
440
|
+
const seen = new Set();
|
|
441
|
+
|
|
442
|
+
for (const operand of compiledOperands) {
|
|
443
|
+
// Create a fresh context copy for each operand
|
|
444
|
+
// This prevents variable definitions from leaking between branches
|
|
445
|
+
const operandCtx = { ...ctx, env: { ...ctx.env } };
|
|
446
|
+
const operandResult = operand.fn(operandCtx);
|
|
447
|
+
|
|
448
|
+
// Remove duplicates
|
|
449
|
+
for (const item of operandResult) {
|
|
450
|
+
const key = JSON.stringify(item);
|
|
451
|
+
if (!seen.has(key)) {
|
|
452
|
+
seen.add(key);
|
|
453
|
+
results.push(item);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return results;
|
|
459
|
+
},
|
|
460
|
+
type: this.resolveType('Any'),
|
|
461
|
+
isSingleton: false,
|
|
462
|
+
source: compiledOperands.map(o => o.source || '').join(' | ')
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Compiles membership test (is operator)
|
|
468
|
+
*/
|
|
469
|
+
private compileMembershipTest(node: MembershipTestNode): CompiledExpression {
|
|
470
|
+
// Get the 'is' operator from registry
|
|
471
|
+
const operation = Registry.getByToken(TokenType.IS, 'infix');
|
|
472
|
+
if (!operation || operation.kind !== 'operator') {
|
|
473
|
+
throw new EvaluationError('is operator not found in registry', node.position);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const expression = this.compileNode(node.expression);
|
|
477
|
+
const typeExpr: CompiledExpression = {
|
|
478
|
+
fn: () => [node.targetType],
|
|
479
|
+
type: this.resolveType('String'),
|
|
480
|
+
isSingleton: true,
|
|
481
|
+
source: node.targetType
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
return operation.compile(this, expression, [expression, typeExpr]);
|
|
486
|
+
} catch (error: any) {
|
|
487
|
+
// If the error doesn't have position, add it from the node
|
|
488
|
+
if (error instanceof EvaluationError && !error.position) {
|
|
489
|
+
error.position = node.position;
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Compiles type cast (as operator)
|
|
497
|
+
*/
|
|
498
|
+
private compileTypeCast(node: TypeCastNode): CompiledExpression {
|
|
499
|
+
// Get the 'as' operator from registry
|
|
500
|
+
const operation = Registry.getByToken(TokenType.AS, 'infix');
|
|
501
|
+
if (!operation || operation.kind !== 'operator') {
|
|
502
|
+
throw new EvaluationError('as operator not found in registry', node.position);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const expression = this.compileNode(node.expression);
|
|
506
|
+
const typeExpr: CompiledExpression = {
|
|
507
|
+
fn: () => [node.targetType],
|
|
508
|
+
type: this.resolveType('String'),
|
|
509
|
+
isSingleton: true,
|
|
510
|
+
source: node.targetType
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
return operation.compile(this, expression, [typeExpr]);
|
|
515
|
+
} catch (error: any) {
|
|
516
|
+
// If the error doesn't have position, add it from the node
|
|
517
|
+
if (error instanceof EvaluationError && !error.position) {
|
|
518
|
+
error.position = node.position;
|
|
519
|
+
}
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Compiles type reference - should not be evaluated directly
|
|
526
|
+
*/
|
|
527
|
+
private compileTypeReference(node: TypeReferenceNode): CompiledExpression {
|
|
528
|
+
// Type references are used in ofType() and similar functions
|
|
529
|
+
// They should compile to return the type name as a string
|
|
530
|
+
const typeName = node.typeName;
|
|
531
|
+
return {
|
|
532
|
+
fn: (ctx: RuntimeContext) => [typeName],
|
|
533
|
+
type: this.resolveType('String'),
|
|
534
|
+
isSingleton: true,
|
|
535
|
+
source: typeName
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Helper function to compile a FHIRPath expression
|
|
542
|
+
*/
|
|
543
|
+
export function compile(expression: string | ASTNode): CompiledExpression {
|
|
544
|
+
// Parse if string
|
|
545
|
+
const ast = typeof expression === 'string'
|
|
546
|
+
? require('../parser').parse(expression)
|
|
547
|
+
: expression;
|
|
548
|
+
|
|
549
|
+
// Create compiler and compile
|
|
550
|
+
const compiler = new Compiler();
|
|
551
|
+
return compiler.compile(ast);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Helper function to compile and evaluate a FHIRPath expression
|
|
556
|
+
*/
|
|
557
|
+
export function evaluateCompiled(
|
|
558
|
+
expression: string | ASTNode,
|
|
559
|
+
input: any,
|
|
560
|
+
context?: RuntimeContext
|
|
561
|
+
): any[] {
|
|
562
|
+
// Compile the expression
|
|
563
|
+
const compiled = compile(expression);
|
|
564
|
+
|
|
565
|
+
// Convert input to collection
|
|
566
|
+
const inputCollection = CollectionUtils.toCollection(input);
|
|
567
|
+
|
|
568
|
+
// Create runtime context
|
|
569
|
+
const runtimeContext: RuntimeContext = context || {
|
|
570
|
+
input: inputCollection,
|
|
571
|
+
focus: inputCollection,
|
|
572
|
+
env: {
|
|
573
|
+
$this: inputCollection
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Execute the compiled function
|
|
578
|
+
return compiled.fn(runtimeContext);
|
|
579
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RuntimeContextManager } from '../runtime/context';
|
|
2
|
+
import type { RuntimeContext } from '../runtime/context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adapter functions to help transition compiler operations to prototype-based context.
|
|
6
|
+
* These can be used to gradually migrate operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an iterator context using prototype inheritance
|
|
11
|
+
* instead of spread operator copying.
|
|
12
|
+
*/
|
|
13
|
+
export function createIteratorContext(
|
|
14
|
+
ctx: RuntimeContext,
|
|
15
|
+
item: any,
|
|
16
|
+
index: number
|
|
17
|
+
): RuntimeContext {
|
|
18
|
+
return RuntimeContextManager.withIterator(ctx, item, index);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a context with new input/focus using prototype inheritance
|
|
23
|
+
*/
|
|
24
|
+
export function createInputContext(
|
|
25
|
+
ctx: RuntimeContext,
|
|
26
|
+
input: any[],
|
|
27
|
+
focus?: any[]
|
|
28
|
+
): RuntimeContext {
|
|
29
|
+
return RuntimeContextManager.withInput(ctx, input, focus);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Set a variable in context using prototype inheritance
|
|
34
|
+
*/
|
|
35
|
+
export function setContextVariable(
|
|
36
|
+
ctx: RuntimeContext,
|
|
37
|
+
name: string,
|
|
38
|
+
value: any[]
|
|
39
|
+
): RuntimeContext {
|
|
40
|
+
return RuntimeContextManager.setVariable(ctx, name, value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Example of how to update the where operation to use prototype-based context
|
|
45
|
+
*/
|
|
46
|
+
export function whereWithPrototypeContext(
|
|
47
|
+
input: { fn: (ctx: RuntimeContext) => any[] },
|
|
48
|
+
criteria: { fn: (ctx: RuntimeContext) => any[] }
|
|
49
|
+
) {
|
|
50
|
+
return {
|
|
51
|
+
fn: (ctx: RuntimeContext) => {
|
|
52
|
+
const inputValue = input.fn(ctx);
|
|
53
|
+
const results: any[] = [];
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < inputValue.length; i++) {
|
|
56
|
+
const item = inputValue[i];
|
|
57
|
+
// Use prototype-based context instead of spread operator
|
|
58
|
+
const iterCtx = createIteratorContext(ctx, item, i);
|
|
59
|
+
const predicateResult = criteria.fn(iterCtx);
|
|
60
|
+
|
|
61
|
+
if (isTruthy(predicateResult)) {
|
|
62
|
+
results.push(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Example of how to update the select operation to use prototype-based context
|
|
73
|
+
*/
|
|
74
|
+
export function selectWithPrototypeContext(
|
|
75
|
+
input: { fn: (ctx: RuntimeContext) => any[] },
|
|
76
|
+
expression: { fn: (ctx: RuntimeContext) => any[] }
|
|
77
|
+
) {
|
|
78
|
+
return {
|
|
79
|
+
fn: (ctx: RuntimeContext) => {
|
|
80
|
+
const inputValue = input.fn(ctx);
|
|
81
|
+
const results: any[] = [];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < inputValue.length; i++) {
|
|
84
|
+
const item = inputValue[i];
|
|
85
|
+
// Use prototype-based context instead of spread operator
|
|
86
|
+
const iterCtx = createIteratorContext(ctx, item, i);
|
|
87
|
+
const exprResult = expression.fn(iterCtx);
|
|
88
|
+
results.push(...exprResult);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Helper function (should be imported from utils)
|
|
97
|
+
function isTruthy(value: any[]): boolean {
|
|
98
|
+
return value.length > 0 && value[0] === true;
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Context, EvaluationResult } from '../interpreter/types';
|
|
2
|
+
import type { ASTNode } from '../parser/ast';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A compiled FHIRPath node - a JavaScript function that implements
|
|
6
|
+
* the stream-processing model: (input, context) → (output, new context)
|
|
7
|
+
*/
|
|
8
|
+
export type CompiledNode = (input: any[], context: Context) => EvaluationResult;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A function that compiles a specific AST node type into a JavaScript closure
|
|
12
|
+
*/
|
|
13
|
+
export type NodeCompiler<T extends ASTNode = ASTNode> = (node: T) => CompiledNode;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compilation context for tracking state during compilation
|
|
17
|
+
*/
|
|
18
|
+
export interface CompilationContext {
|
|
19
|
+
// Future: optimization flags, source map info, etc.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Re-export types from registry to avoid conflicts
|
|
23
|
+
export type { CompiledExpression, RuntimeContext } from '../registry/types';
|