@atomic-ehr/fhirpath 0.0.1-canary.0c6931e.20250727185306

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