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