@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,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,2 @@
1
+ export { Compiler, compile, evaluateCompiled } from './compiler';
2
+ export type { CompiledNode, NodeCompiler, CompilationContext } from './types';
@@ -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`