@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.
Files changed (57) hide show
  1. package/README.md +307 -0
  2. package/dist/index.d.ts +225 -0
  3. package/dist/index.js +8185 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/analyzer/analyzer.ts +486 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +148 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +152 -0
  14. package/src/api/index.ts +57 -0
  15. package/src/api/registry.ts +128 -0
  16. package/src/api/types.ts +154 -0
  17. package/src/compiler/compiler.ts +579 -0
  18. package/src/compiler/index.ts +2 -0
  19. package/src/compiler/prototype-context-adapter.ts +99 -0
  20. package/src/compiler/types.ts +23 -0
  21. package/src/index.ts +52 -0
  22. package/src/interpreter/README.md +78 -0
  23. package/src/interpreter/interpreter.ts +485 -0
  24. package/src/interpreter/types.ts +110 -0
  25. package/src/lexer/char-tables.ts +37 -0
  26. package/src/lexer/errors.ts +31 -0
  27. package/src/lexer/index.ts +5 -0
  28. package/src/lexer/lexer.ts +745 -0
  29. package/src/lexer/token.ts +104 -0
  30. package/src/parser/ast.ts +123 -0
  31. package/src/parser/index.ts +3 -0
  32. package/src/parser/parser.ts +701 -0
  33. package/src/parser/pprint.ts +169 -0
  34. package/src/registry/default-analyzers.ts +257 -0
  35. package/src/registry/default-compilers.ts +31 -0
  36. package/src/registry/index.ts +93 -0
  37. package/src/registry/operations/arithmetic.ts +506 -0
  38. package/src/registry/operations/collection.ts +425 -0
  39. package/src/registry/operations/comparison.ts +432 -0
  40. package/src/registry/operations/existence.ts +703 -0
  41. package/src/registry/operations/filtering.ts +358 -0
  42. package/src/registry/operations/literals.ts +341 -0
  43. package/src/registry/operations/logical.ts +402 -0
  44. package/src/registry/operations/math.ts +128 -0
  45. package/src/registry/operations/membership.ts +132 -0
  46. package/src/registry/operations/string.ts +507 -0
  47. package/src/registry/operations/subsetting.ts +174 -0
  48. package/src/registry/operations/type-checking.ts +162 -0
  49. package/src/registry/operations/type-conversion.ts +404 -0
  50. package/src/registry/operations/type-operators.ts +307 -0
  51. package/src/registry/operations/utility.ts +542 -0
  52. package/src/registry/registry.ts +146 -0
  53. package/src/registry/types.ts +161 -0
  54. package/src/registry/utils/evaluation-helpers.ts +93 -0
  55. package/src/registry/utils/index.ts +3 -0
  56. package/src/registry/utils/type-system.ts +173 -0
  57. package/src/runtime/context.ts +179 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@atomic-ehr/fhirpath",
3
+ "version": "0.0.1-canary.35b105d.20250724165800",
4
+ "description": "A TypeScript implementation of FHIRPath",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "bun test",
22
+ "typecheck": "bunx tsc --noEmit",
23
+ "prepublishOnly": "bun run build"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/atomic-ehr/fhirpath.git"
28
+ },
29
+ "keywords": [
30
+ "fhir",
31
+ "fhirpath",
32
+ "healthcare",
33
+ "typescript"
34
+ ],
35
+ "author": "Atomic EHR Team",
36
+ "license": "MIT",
37
+ "bugs": {
38
+ "url": "https://github.com/atomic-ehr/fhirpath/issues"
39
+ },
40
+ "homepage": "https://github.com/atomic-ehr/fhirpath#readme",
41
+ "devDependencies": {
42
+ "@rgrove/parse-xml": "^4.2.0",
43
+ "@types/bun": "latest",
44
+ "@types/node": "22.13.14",
45
+ "tsup": "8.5.0",
46
+ "typescript": "^5"
47
+ },
48
+ "peerDependencies": {
49
+ "typescript": "^5"
50
+ }
51
+ }
@@ -0,0 +1,486 @@
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
+ Position
17
+ } from '../parser/ast';
18
+ import { NodeType } from '../parser/ast';
19
+ import { TokenType } from '../lexer/token';
20
+ import type {
21
+ ModelProvider,
22
+ TypeRef,
23
+ TypeAnalysisResult,
24
+ TypeDiagnostic
25
+ } from './types';
26
+ import { AnalysisMode } from './types';
27
+ import { Registry } from '../registry';
28
+ import type { TypeInfo as RegistryTypeInfo, Analyzer as IAnalyzer } from '../registry/types';
29
+
30
+ // Type for node analyzer functions
31
+ type NodeAnalyzer = (node: any, inputType: TypeRef | undefined, inputIsSingleton: boolean) => AnalysisResult;
32
+
33
+ interface AnalysisResult {
34
+ type: TypeRef | undefined;
35
+ isSingleton: boolean;
36
+ }
37
+
38
+ /**
39
+ * FHIRPath Type Analyzer - performs type analysis on AST nodes
40
+ * Follows the same pattern as the interpreter but tracks types instead of values
41
+ */
42
+ export class TypeAnalyzer implements IAnalyzer {
43
+ private diagnostics: TypeDiagnostic[] = [];
44
+ private currentPosition?: Position;
45
+
46
+ // Object lookup for node analyzers (mirrors interpreter pattern)
47
+ private readonly nodeAnalyzers: Record<NodeType, NodeAnalyzer> = {
48
+ [NodeType.Literal]: this.analyzeLiteral.bind(this),
49
+ [NodeType.Identifier]: this.analyzeIdentifier.bind(this),
50
+ [NodeType.TypeOrIdentifier]: this.analyzeTypeOrIdentifier.bind(this),
51
+ [NodeType.Variable]: this.analyzeVariable.bind(this),
52
+ [NodeType.Binary]: this.analyzeBinary.bind(this),
53
+ [NodeType.Unary]: this.analyzeUnary.bind(this),
54
+ [NodeType.Function]: this.analyzeFunction.bind(this),
55
+ [NodeType.Collection]: this.analyzeCollection.bind(this),
56
+ [NodeType.Index]: this.analyzeIndex.bind(this),
57
+ [NodeType.Union]: this.analyzeUnion.bind(this),
58
+ [NodeType.MembershipTest]: this.analyzeMembershipTest.bind(this),
59
+ [NodeType.TypeCast]: this.analyzeTypeCast.bind(this),
60
+ [NodeType.TypeReference]: this.analyzeTypeReference.bind(this),
61
+ };
62
+
63
+ constructor(
64
+ private modelProvider: ModelProvider,
65
+ private mode: AnalysisMode = AnalysisMode.Lenient
66
+ ) {}
67
+
68
+ // IAnalyzer interface implementation
69
+ error(message: string): void {
70
+ this.addDiagnostic('error', message, this.currentPosition);
71
+ }
72
+
73
+ warning(message: string): void {
74
+ this.addDiagnostic('warning', message, this.currentPosition);
75
+ }
76
+
77
+ resolveType(typeName: string): TypeRef {
78
+ return this.modelProvider.resolveType(typeName) || this.modelProvider.resolveType('Any')!;
79
+ }
80
+
81
+ /**
82
+ * Analyze a FHIRPath expression
83
+ */
84
+ analyze(
85
+ ast: ASTNode,
86
+ inputType?: TypeRef,
87
+ inputIsSingleton: boolean = true
88
+ ): TypeAnalysisResult {
89
+ this.diagnostics = [];
90
+
91
+ const result = this.analyzeNode(ast, inputType, inputIsSingleton);
92
+
93
+ return {
94
+ ast,
95
+ diagnostics: this.diagnostics,
96
+ resultType: result.type,
97
+ resultIsSingleton: result.isSingleton
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Main analysis method - uses object lookup
103
+ */
104
+ private analyzeNode(
105
+ node: ASTNode,
106
+ inputType: TypeRef | undefined,
107
+ inputIsSingleton: boolean
108
+ ): AnalysisResult {
109
+ const analyzer = this.nodeAnalyzers[node.type];
110
+
111
+ if (!analyzer) {
112
+ this.addDiagnostic('error', `Unknown node type: ${node.type}`, node.position);
113
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
114
+ }
115
+
116
+ const result = analyzer(node, inputType, inputIsSingleton);
117
+
118
+ // Annotate the node with type information
119
+ node.resultType = result.type;
120
+ node.isSingleton = result.isSingleton;
121
+
122
+ return result;
123
+ }
124
+
125
+ private analyzeLiteral(node: LiteralNode): AnalysisResult {
126
+ // If literal has operation reference from parser
127
+ if (node.operation && node.operation.kind === 'literal') {
128
+ const inputInfo: RegistryTypeInfo = { type: this.resolveType('Any'), isSingleton: true };
129
+ this.currentPosition = node.position;
130
+ const result = node.operation.analyze(this, inputInfo, []);
131
+ return {
132
+ type: result.type,
133
+ isSingleton: result.isSingleton
134
+ };
135
+ }
136
+
137
+ // Fallback for legacy literals
138
+ let typeName: string;
139
+
140
+ switch (node.valueType) {
141
+ case 'string':
142
+ typeName = 'String';
143
+ break;
144
+ case 'number':
145
+ typeName = Number.isInteger(node.value) ? 'Integer' : 'Decimal';
146
+ break;
147
+ case 'boolean':
148
+ typeName = 'Boolean';
149
+ break;
150
+ case 'date':
151
+ typeName = 'Date';
152
+ break;
153
+ case 'time':
154
+ typeName = 'Time';
155
+ break;
156
+ case 'datetime':
157
+ typeName = 'DateTime';
158
+ break;
159
+ case 'null':
160
+ // null is empty collection
161
+ return { type: undefined, isSingleton: false };
162
+ default:
163
+ typeName = 'Any';
164
+ }
165
+
166
+ return {
167
+ type: this.modelProvider.resolveType(typeName),
168
+ isSingleton: true
169
+ };
170
+ }
171
+
172
+ private analyzeIdentifier(
173
+ node: IdentifierNode,
174
+ inputType: TypeRef | undefined,
175
+ inputIsSingleton: boolean
176
+ ): AnalysisResult {
177
+ if (!inputType) {
178
+ // No input type - might be a type name or error
179
+ const typeRef = this.modelProvider.resolveType(node.name);
180
+ if (typeRef) {
181
+ return { type: typeRef, isSingleton: true };
182
+ }
183
+
184
+ this.addDiagnostic('error', `Cannot navigate property '${node.name}' on empty input`, node.position);
185
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
186
+ }
187
+
188
+ // Property navigation
189
+ const propInfo = this.modelProvider.getPropertyType(inputType, node.name);
190
+
191
+ if (!propInfo) {
192
+ this.addDiagnostic(
193
+ this.mode === AnalysisMode.Strict ? 'error' : 'warning',
194
+ `Property '${node.name}' not found on type '${this.modelProvider.getTypeName(inputType)}'`,
195
+ node.position
196
+ );
197
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
198
+ }
199
+
200
+ // If input is collection, result is always collection (flattening)
201
+ return {
202
+ type: propInfo.type,
203
+ isSingleton: inputIsSingleton && propInfo.isSingleton
204
+ };
205
+ }
206
+
207
+ private analyzeTypeOrIdentifier(
208
+ node: TypeOrIdentifierNode,
209
+ inputType: TypeRef | undefined,
210
+ inputIsSingleton: boolean
211
+ ): AnalysisResult {
212
+ // First try as type reference
213
+ const typeRef = this.modelProvider.resolveType(node.name);
214
+ if (typeRef && !inputType) {
215
+ return { type: typeRef, isSingleton: true };
216
+ }
217
+
218
+ // Otherwise treat as identifier
219
+ return this.analyzeIdentifier(node as any, inputType, inputIsSingleton);
220
+ }
221
+
222
+ private analyzeVariable(node: VariableNode): AnalysisResult {
223
+ // For now, assume variables can be Any type
224
+ // In a real implementation, we'd track variable types in context
225
+ return {
226
+ type: this.modelProvider.resolveType('Any'),
227
+ isSingleton: node.name === '$index' // $index is always singleton
228
+ };
229
+ }
230
+
231
+ private analyzeBinary(
232
+ node: BinaryNode,
233
+ inputType: TypeRef | undefined,
234
+ inputIsSingleton: boolean
235
+ ): AnalysisResult {
236
+ // Special handling for dot operator - it's a pipeline
237
+ if (node.operator === TokenType.DOT) {
238
+ const leftResult = this.analyzeNode(node.left, inputType, inputIsSingleton);
239
+ const rightResult = this.analyzeNode(node.right, leftResult.type, leftResult.isSingleton);
240
+ return rightResult;
241
+ }
242
+
243
+ // Get operation from registry
244
+ const operation = node.operation || Registry.getByToken(node.operator);
245
+ if (!operation || operation.kind !== 'operator') {
246
+ this.addDiagnostic('error', `Unknown operator: ${node.operator}`, node.position);
247
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
248
+ }
249
+
250
+ // Analyze operands
251
+ const leftResult = this.analyzeNode(node.left, inputType, inputIsSingleton);
252
+ const rightResult = this.analyzeNode(node.right, inputType, inputIsSingleton);
253
+
254
+ // Convert to registry TypeInfo format
255
+ const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
256
+ const leftInfo: RegistryTypeInfo = { type: leftResult.type || this.resolveType('Any'), isSingleton: leftResult.isSingleton };
257
+ const rightInfo: RegistryTypeInfo = { type: rightResult.type || this.resolveType('Any'), isSingleton: rightResult.isSingleton };
258
+
259
+ // Use operation's analyze method
260
+ this.currentPosition = node.position;
261
+ const result = operation.analyze(this, inputInfo, [leftInfo, rightInfo]);
262
+
263
+ return {
264
+ type: result.type,
265
+ isSingleton: result.isSingleton
266
+ };
267
+ }
268
+
269
+ private analyzeUnary(
270
+ node: UnaryNode,
271
+ inputType: TypeRef | undefined,
272
+ inputIsSingleton: boolean
273
+ ): AnalysisResult {
274
+ // Get operation from registry
275
+ const operation = node.operation || Registry.getByToken(node.operator);
276
+ if (!operation || operation.kind !== 'operator') {
277
+ this.addDiagnostic('error', `Unknown unary operator: ${node.operator}`, node.position);
278
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
279
+ }
280
+
281
+ // Analyze operand
282
+ const operandResult = this.analyzeNode(node.operand, inputType, inputIsSingleton);
283
+
284
+ // Convert to registry TypeInfo format
285
+ const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
286
+ const operandInfo: RegistryTypeInfo = { type: operandResult.type || this.resolveType('Any'), isSingleton: operandResult.isSingleton };
287
+
288
+ // Use operation's analyze method
289
+ this.currentPosition = node.position;
290
+ const result = operation.analyze(this, inputInfo, [operandInfo]);
291
+
292
+ return {
293
+ type: result.type,
294
+ isSingleton: result.isSingleton
295
+ };
296
+ }
297
+
298
+ private analyzeFunction(
299
+ node: FunctionNode,
300
+ inputType: TypeRef | undefined,
301
+ inputIsSingleton: boolean
302
+ ): AnalysisResult {
303
+ // Extract function name
304
+ let funcName: string;
305
+ if (node.name.type === NodeType.Identifier) {
306
+ funcName = (node.name as IdentifierNode).name;
307
+ } else {
308
+ this.addDiagnostic('error', 'Complex function names not yet supported', node.position);
309
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
310
+ }
311
+
312
+ // Get function from registry
313
+ const operation = Registry.get(funcName);
314
+ if (!operation || operation.kind !== 'function') {
315
+ this.addDiagnostic('error', `Unknown function: ${funcName}`, node.position);
316
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
317
+ }
318
+
319
+ // Analyze arguments
320
+ const argResults = node.arguments.map(arg => this.analyzeNode(arg, inputType, inputIsSingleton));
321
+
322
+ // Convert to registry TypeInfo format
323
+ const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
324
+ const argInfos: RegistryTypeInfo[] = argResults.map(r => ({
325
+ type: r.type || this.resolveType('Any'),
326
+ isSingleton: r.isSingleton
327
+ }));
328
+
329
+ // Use operation's analyze method
330
+ this.currentPosition = node.position;
331
+ const result = operation.analyze(this, inputInfo, argInfos);
332
+
333
+ return {
334
+ type: result.type,
335
+ isSingleton: result.isSingleton
336
+ };
337
+ }
338
+
339
+ private analyzeCollection(
340
+ node: CollectionNode,
341
+ inputType: TypeRef | undefined,
342
+ inputIsSingleton: boolean
343
+ ): AnalysisResult {
344
+ if (node.elements.length === 0) {
345
+ // Empty collection
346
+ return { type: undefined, isSingleton: false };
347
+ }
348
+
349
+ // Analyze all elements
350
+ const elementTypes: TypeRef[] = [];
351
+
352
+ for (const element of node.elements) {
353
+ const result = this.analyzeNode(element, inputType, inputIsSingleton);
354
+ if (result.type) {
355
+ elementTypes.push(result.type);
356
+ }
357
+ }
358
+
359
+ // Get common type
360
+ const commonType = this.modelProvider.getCommonType?.(elementTypes) || this.modelProvider.resolveType('Any');
361
+
362
+ return { type: commonType, isSingleton: false };
363
+ }
364
+
365
+ private analyzeIndex(
366
+ node: IndexNode,
367
+ inputType: TypeRef | undefined,
368
+ inputIsSingleton: boolean
369
+ ): AnalysisResult {
370
+ // Analyze the expression being indexed
371
+ const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
372
+
373
+ // Analyze the index expression
374
+ const indexResult = this.analyzeNode(node.index, exprResult.type, exprResult.isSingleton);
375
+
376
+ // Index must be Integer
377
+ if (indexResult.type) {
378
+ const typeName = this.modelProvider.getTypeName(indexResult.type);
379
+ if (typeName !== 'Integer') {
380
+ this.addDiagnostic('error', 'Index must be an integer', node.position);
381
+ }
382
+ }
383
+
384
+ if (!indexResult.isSingleton) {
385
+ this.addDiagnostic('error', 'Index must be singleton', node.position);
386
+ }
387
+
388
+ // Result is singleton of the expression type
389
+ return { type: exprResult.type, isSingleton: true };
390
+ }
391
+
392
+ private analyzeUnion(
393
+ node: UnionNode,
394
+ inputType: TypeRef | undefined,
395
+ inputIsSingleton: boolean
396
+ ): AnalysisResult {
397
+ const types: TypeRef[] = [];
398
+
399
+ for (const operand of node.operands) {
400
+ const result = this.analyzeNode(operand, inputType, inputIsSingleton);
401
+ if (result.type) {
402
+ types.push(result.type);
403
+ }
404
+ }
405
+
406
+ const commonType = this.modelProvider.getCommonType?.(types) || this.modelProvider.resolveType('Any');
407
+
408
+ return { type: commonType, isSingleton: false };
409
+ }
410
+
411
+ private analyzeMembershipTest(
412
+ node: MembershipTestNode,
413
+ inputType: TypeRef | undefined,
414
+ inputIsSingleton: boolean
415
+ ): AnalysisResult {
416
+ // Analyze the expression
417
+ const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
418
+
419
+ // Result is Boolean with same cardinality as input
420
+ return {
421
+ type: this.modelProvider.resolveType('Boolean'),
422
+ isSingleton: exprResult.isSingleton
423
+ };
424
+ }
425
+
426
+ private analyzeTypeCast(
427
+ node: TypeCastNode,
428
+ inputType: TypeRef | undefined,
429
+ inputIsSingleton: boolean
430
+ ): AnalysisResult {
431
+ // Analyze the expression
432
+ const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
433
+
434
+ // Resolve target type
435
+ const targetType = this.modelProvider.resolveType(node.targetType);
436
+
437
+ if (!targetType) {
438
+ this.addDiagnostic('error', `Unknown type: ${node.targetType}`, node.position);
439
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: exprResult.isSingleton };
440
+ }
441
+
442
+ // Result has target type with same cardinality
443
+ return {
444
+ type: targetType,
445
+ isSingleton: exprResult.isSingleton
446
+ };
447
+ }
448
+
449
+ private analyzeTypeReference(node: TypeReferenceNode): AnalysisResult {
450
+ const typeRef = this.modelProvider.resolveType(node.typeName);
451
+
452
+ if (!typeRef) {
453
+ this.addDiagnostic('error', `Unknown type: ${node.typeName}`, node.position);
454
+ return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
455
+ }
456
+
457
+ return { type: typeRef, isSingleton: true };
458
+ }
459
+
460
+ private addDiagnostic(
461
+ severity: 'error' | 'warning',
462
+ message: string,
463
+ position?: Position
464
+ ) {
465
+ this.diagnostics.push({ severity, message, position });
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Helper function to analyze a FHIRPath expression
471
+ */
472
+ export function analyzeFHIRPath(
473
+ expression: string | ASTNode,
474
+ modelProvider: ModelProvider,
475
+ inputType?: TypeRef,
476
+ mode: AnalysisMode = AnalysisMode.Lenient
477
+ ): TypeAnalysisResult {
478
+ // Parse if string
479
+ const ast = typeof expression === 'string'
480
+ ? require('../parser').parse(expression)
481
+ : expression;
482
+
483
+ // Create analyzer and analyze
484
+ const analyzer = new TypeAnalyzer(modelProvider, mode);
485
+ return analyzer.analyze(ast, inputType);
486
+ }