@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.261bb9b.20250811212042

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 (104) hide show
  1. package/README.md +62 -56
  2. package/dist/index.d.ts +368 -99
  3. package/dist/index.js +3623 -1253
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/analyzer.ts +779 -120
  7. package/src/boxing.ts +124 -0
  8. package/src/completion-provider.ts +636 -0
  9. package/src/cursor-nodes.ts +111 -0
  10. package/src/errors.ts +247 -0
  11. package/src/index.ts +97 -14
  12. package/src/inspect.ts +309 -176
  13. package/src/interpreter.ts +380 -71
  14. package/src/lexer.ts +1 -0
  15. package/src/model-provider.ts +282 -39
  16. package/src/operations/abs-function.ts +40 -16
  17. package/src/operations/aggregate-function.ts +21 -10
  18. package/src/operations/all-function.ts +23 -11
  19. package/src/operations/allFalse-function.ts +14 -9
  20. package/src/operations/allTrue-function.ts +14 -9
  21. package/src/operations/and-operator.ts +21 -12
  22. package/src/operations/anyFalse-function.ts +11 -7
  23. package/src/operations/anyTrue-function.ts +11 -7
  24. package/src/operations/as-operator.ts +2 -1
  25. package/src/operations/ceiling-function.ts +14 -8
  26. package/src/operations/children-function.ts +98 -0
  27. package/src/operations/combine-function.ts +10 -6
  28. package/src/operations/combine-operator.ts +19 -5
  29. package/src/operations/contains-function.ts +26 -12
  30. package/src/operations/contains-operator.ts +15 -7
  31. package/src/operations/count-function.ts +7 -4
  32. package/src/operations/defineVariable-function.ts +11 -7
  33. package/src/operations/descendants-function.ts +64 -0
  34. package/src/operations/distinct-function.ts +21 -7
  35. package/src/operations/div-operator.ts +16 -3
  36. package/src/operations/divide-operator.ts +11 -6
  37. package/src/operations/dot-operator.ts +4 -2
  38. package/src/operations/empty-function.ts +7 -4
  39. package/src/operations/endsWith-function.ts +28 -14
  40. package/src/operations/equal-operator.ts +19 -7
  41. package/src/operations/equivalent-operator.ts +16 -11
  42. package/src/operations/exclude-function.ts +18 -14
  43. package/src/operations/exists-function.ts +19 -10
  44. package/src/operations/first-function.ts +12 -7
  45. package/src/operations/floor-function.ts +14 -8
  46. package/src/operations/greater-operator.ts +10 -5
  47. package/src/operations/greater-or-equal-operator.ts +10 -5
  48. package/src/operations/iif-function.ts +27 -23
  49. package/src/operations/implies-operator.ts +23 -8
  50. package/src/operations/in-operator.ts +18 -8
  51. package/src/operations/index.ts +3 -0
  52. package/src/operations/indexOf-function.ts +28 -14
  53. package/src/operations/intersect-function.ts +23 -24
  54. package/src/operations/is-operator.ts +67 -15
  55. package/src/operations/isDistinct-function.ts +17 -9
  56. package/src/operations/join-function.ts +22 -9
  57. package/src/operations/last-function.ts +12 -7
  58. package/src/operations/length-function.ts +17 -8
  59. package/src/operations/less-operator.ts +10 -5
  60. package/src/operations/less-or-equal-operator.ts +10 -5
  61. package/src/operations/less-than.ts +26 -2
  62. package/src/operations/lower-function.ts +17 -8
  63. package/src/operations/minus-operator.ts +10 -5
  64. package/src/operations/mod-operator.ts +20 -3
  65. package/src/operations/multiply-operator.ts +12 -7
  66. package/src/operations/not-equal-operator.ts +16 -7
  67. package/src/operations/not-equivalent-operator.ts +16 -11
  68. package/src/operations/not-function.ts +17 -7
  69. package/src/operations/ofType-function.ts +139 -0
  70. package/src/operations/or-operator.ts +21 -12
  71. package/src/operations/plus-operator.ts +19 -7
  72. package/src/operations/power-function.ts +27 -13
  73. package/src/operations/replace-function.ts +33 -14
  74. package/src/operations/round-function.ts +29 -12
  75. package/src/operations/select-function.ts +17 -9
  76. package/src/operations/single-function.ts +10 -6
  77. package/src/operations/skip-function.ts +18 -9
  78. package/src/operations/split-function.ts +30 -13
  79. package/src/operations/sqrt-function.ts +14 -8
  80. package/src/operations/startsWith-function.ts +26 -12
  81. package/src/operations/subsetOf-function.ts +20 -15
  82. package/src/operations/substring-function.ts +43 -24
  83. package/src/operations/supersetOf-function.ts +20 -15
  84. package/src/operations/tail-function.ts +8 -4
  85. package/src/operations/take-function.ts +18 -9
  86. package/src/operations/toBoolean-function.ts +23 -14
  87. package/src/operations/toDecimal-function.ts +18 -9
  88. package/src/operations/toInteger-function.ts +22 -12
  89. package/src/operations/toString-function.ts +26 -16
  90. package/src/operations/trace-function.ts +19 -10
  91. package/src/operations/trim-function.ts +16 -7
  92. package/src/operations/truncate-function.ts +14 -8
  93. package/src/operations/unary-minus-operator.ts +23 -13
  94. package/src/operations/unary-plus-operator.ts +2 -1
  95. package/src/operations/union-function.ts +25 -28
  96. package/src/operations/union-operator.ts +2 -1
  97. package/src/operations/upper-function.ts +17 -8
  98. package/src/operations/where-function.ts +20 -11
  99. package/src/operations/xor-operator.ts +9 -4
  100. package/src/parser.ts +527 -8
  101. package/src/quantity-value.ts +4 -8
  102. package/src/registry.ts +147 -5
  103. package/src/types.ts +42 -19
  104. package/src/parser-base.ts +0 -400
package/src/analyzer.ts CHANGED
@@ -20,34 +20,53 @@ import type {
20
20
  } from './types';
21
21
  import { NodeType, DiagnosticSeverity } from './types';
22
22
  import { registry } from './registry';
23
+ import { Errors, toDiagnostic } from './errors';
24
+ import { isCursorNode, CursorContext } from './cursor-nodes';
25
+ import type { AnyCursorNode } from './cursor-nodes';
23
26
 
24
- // Diagnostic codes as constants to avoid magic strings
25
- const DiagnosticCode = {
26
- UnknownOperator: 'UNKNOWN_OPERATOR',
27
- UnknownVariable: 'UNKNOWN_VARIABLE',
28
- UnknownUserVariable: 'UNKNOWN_USER_VARIABLE',
29
- UnknownFunction: 'UNKNOWN_FUNCTION',
30
- TooFewArgs: 'TOO_FEW_ARGS',
31
- TooManyArgs: 'TOO_MANY_ARGS',
32
- TypeMismatch: 'TYPE_MISMATCH',
33
- InputTypeMismatch: 'INPUT_TYPE_MISMATCH',
34
- ArgumentTypeMismatch: 'ARGUMENT_TYPE_MISMATCH',
35
- ParseError: 'PARSE_ERROR',
36
- } as const;
27
+
28
+ export interface AnalyzerOptions {
29
+ cursorMode?: boolean;
30
+ }
31
+
32
+ export interface AnalysisResultWithCursor extends AnalysisResult {
33
+ stoppedAtCursor?: boolean;
34
+ cursorContext?: {
35
+ typeBeforeCursor?: TypeInfo;
36
+ expectedType?: TypeInfo;
37
+ cursorNode?: AnyCursorNode;
38
+ };
39
+ }
37
40
 
38
41
  export class Analyzer {
39
42
  private diagnostics: Diagnostic[] = [];
40
- private variables: Set<string> = new Set(['$this', '$index', '$total']);
43
+ private variables: Set<string> = new Set(['$this', '$index', '$total', 'context', 'resource', 'rootResource']);
41
44
  private modelProvider?: ModelProvider;
42
45
  private userVariableTypes: Map<string, TypeInfo> = new Map();
46
+ private systemVariableTypes: Map<string, TypeInfo> = new Map();
47
+ private cursorMode: boolean = false;
48
+ private stoppedAtCursor: boolean = false;
49
+ private cursorContext?: {
50
+ typeBeforeCursor?: TypeInfo;
51
+ expectedType?: TypeInfo;
52
+ cursorNode?: AnyCursorNode;
53
+ };
43
54
 
44
55
  constructor(modelProvider?: ModelProvider) {
45
56
  this.modelProvider = modelProvider;
46
57
  }
47
58
 
48
- analyze(ast: ASTNode, userVariables?: Record<string, any>, inputType?: TypeInfo): AnalysisResult {
59
+ async analyze(
60
+ ast: ASTNode,
61
+ userVariables?: Record<string, any>,
62
+ inputType?: TypeInfo,
63
+ options?: AnalyzerOptions
64
+ ): Promise<AnalysisResultWithCursor> {
49
65
  this.diagnostics = [];
50
66
  this.userVariableTypes.clear();
67
+ this.cursorMode = options?.cursorMode ?? false;
68
+ this.stoppedAtCursor = false;
69
+ this.cursorContext = undefined;
51
70
 
52
71
  if (userVariables) {
53
72
  Object.keys(userVariables).forEach(name => {
@@ -61,24 +80,43 @@ export class Analyzer {
61
80
  }
62
81
 
63
82
  // Annotate AST with type information
64
- this.annotateAST(ast, inputType);
83
+ await this.annotateAST(ast, inputType);
65
84
 
66
- // Perform validation with type checking
67
- this.visitNode(ast);
85
+ // Perform validation with type checking (if not stopped at cursor)
86
+ if (!this.stoppedAtCursor) {
87
+ this.visitNode(ast);
88
+ }
68
89
 
69
90
  return {
70
91
  diagnostics: this.diagnostics,
71
- ast
92
+ ast,
93
+ stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
94
+ cursorContext: this.cursorMode ? this.cursorContext : undefined
72
95
  };
73
96
  }
74
97
 
75
98
  private visitNode(node: ASTNode): void {
99
+ // Check for cursor node in cursor mode
100
+ if (this.cursorMode && isCursorNode(node)) {
101
+ this.stoppedAtCursor = true;
102
+ this.cursorContext = {
103
+ cursorNode: node as AnyCursorNode,
104
+ typeBeforeCursor: (node as any).typeInfo
105
+ };
106
+ return; // Short-circuit
107
+ }
108
+
76
109
  // Handle error nodes - process them for diagnostics but don't traverse
77
110
  if (node.type === 'Error') {
78
111
  // Diagnostics already added in annotateAST
79
112
  return;
80
113
  }
81
114
 
115
+ // If we've already stopped at cursor, don't continue
116
+ if (this.stoppedAtCursor) {
117
+ return;
118
+ }
119
+
82
120
  switch (node.type) {
83
121
  case NodeType.Binary:
84
122
  this.visitBinaryOperator(node as BinaryNode);
@@ -101,10 +139,10 @@ export class Analyzer {
101
139
  this.visitNode((node as UnaryNode).operand);
102
140
  break;
103
141
  case NodeType.MembershipTest:
104
- this.visitNode((node as MembershipTestNode).expression);
142
+ this.visitMembershipTest(node as MembershipTestNode);
105
143
  break;
106
144
  case NodeType.TypeCast:
107
- this.visitNode((node as TypeCastNode).expression);
145
+ this.visitTypeCast(node as TypeCastNode);
108
146
  break;
109
147
  case NodeType.Variable:
110
148
  this.validateVariable((node as VariableNode).name, node);
@@ -120,22 +158,84 @@ export class Analyzer {
120
158
  private visitBinaryOperator(node: BinaryNode): void {
121
159
  this.visitNode(node.left);
122
160
 
161
+ // Track defineVariable for validation - collect all variables defined in the chain
162
+ if (node.operator === '.') {
163
+ const definedVars = this.collectDefinedVariables(node.left);
164
+ if (definedVars.size > 0) {
165
+ // Track which variables were already known
166
+ const previouslyKnown = new Set<string>();
167
+ definedVars.forEach(varName => {
168
+ if (this.variables.has(varName)) {
169
+ previouslyKnown.add(varName);
170
+ }
171
+ this.variables.add(varName);
172
+ });
173
+
174
+ // Visit right side with new variables in scope
175
+ this.visitNode(node.right);
176
+
177
+ // Restore previous state
178
+ definedVars.forEach(varName => {
179
+ if (!previouslyKnown.has(varName)) {
180
+ this.variables.delete(varName);
181
+ }
182
+ });
183
+ return;
184
+ }
185
+ }
186
+
123
187
  // Special handling for dot operator with function on right side
124
188
  if (node.operator === '.' && node.right.type === NodeType.Function) {
125
189
  const funcNode = node.right as FunctionNode;
126
190
  if (funcNode.name.type === NodeType.Identifier) {
127
191
  const funcName = (funcNode.name as IdentifierNode).name;
128
192
  const func = registry.getFunction(funcName);
129
- if (func && func.signature.input && node.left.typeInfo) {
130
- if (!this.isTypeCompatible(node.left.typeInfo, func.signature.input)) {
193
+ if (func && func.signatures && func.signatures.length > 0 && node.left.typeInfo) {
194
+ // Check if any signature matches the input type
195
+ let matchFound = false;
196
+ let expectedTypes: string[] = [];
197
+
198
+ for (const signature of func.signatures) {
199
+ if (signature.input) {
200
+ if (this.isTypeCompatible(node.left.typeInfo, signature.input)) {
201
+ matchFound = true;
202
+ break;
203
+ }
204
+ expectedTypes.push(this.typeToString(signature.input));
205
+ } else {
206
+ // If any signature has no input constraint, it matches
207
+ matchFound = true;
208
+ break;
209
+ }
210
+ }
211
+
212
+ if (!matchFound) {
131
213
  const inputTypeStr = this.typeToString(node.left.typeInfo);
132
- const expectedTypeStr = this.typeToString(func.signature.input);
133
- this.addDiagnostic(
134
- DiagnosticSeverity.Error,
135
- `Type mismatch: function '${func.name}' expects input type ${expectedTypeStr} but got ${inputTypeStr}`,
136
- funcNode,
137
- DiagnosticCode.InputTypeMismatch
214
+ const firstSignature = func.signatures[0];
215
+
216
+ if (!firstSignature) return;
217
+
218
+ // Check if this is specifically a singleton/collection mismatch
219
+ const inputIsCollection = !node.left.typeInfo.singleton;
220
+ const expectedIsSingleton = firstSignature.input?.singleton;
221
+
222
+ // Check if the base types are compatible (same type or subtype)
223
+ const typesCompatible = firstSignature.input && (
224
+ node.left.typeInfo.type === firstSignature.input.type ||
225
+ this.isSubtypeOf(node.left.typeInfo.type, firstSignature.input.type)
138
226
  );
227
+
228
+ if (inputIsCollection && expectedIsSingleton && typesCompatible) {
229
+ // Compatible base types but collection vs singleton mismatch
230
+ this.diagnostics.push(
231
+ toDiagnostic(Errors.singletonTypeRequired(funcName, inputTypeStr, funcNode.range))
232
+ );
233
+ } else {
234
+ // Function received invalid operand type - report as runtime error
235
+ this.diagnostics.push(
236
+ toDiagnostic(Errors.invalidOperandType(funcName + '()', inputTypeStr, funcNode.range))
237
+ );
238
+ }
139
239
  }
140
240
  }
141
241
  }
@@ -150,7 +250,9 @@ export class Analyzer {
150
250
 
151
251
  const op = registry.getOperatorDefinition(node.operator);
152
252
  if (!op) {
153
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown operator: ${node.operator}`, node, DiagnosticCode.UnknownOperator);
253
+ this.diagnostics.push(
254
+ toDiagnostic(Errors.unknownOperator(node.operator, node.range))
255
+ );
154
256
  return;
155
257
  }
156
258
 
@@ -167,20 +269,87 @@ export class Analyzer {
167
269
  private visitFunctionCall(node: FunctionNode): void {
168
270
  if (node.name.type === NodeType.Identifier) {
169
271
  const funcName = (node.name as IdentifierNode).name;
272
+
273
+ // Check if this is a type operation that requires ModelProvider
274
+ if (funcName === 'ofType' && !this.modelProvider) {
275
+ // Check if the type argument is a primitive type
276
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
277
+ let isPrimitive = false;
278
+
279
+ if (node.arguments.length > 0) {
280
+ const typeArg = node.arguments[0]!;
281
+ if (typeArg.type === NodeType.Identifier) {
282
+ isPrimitive = primitiveTypes.includes((typeArg as IdentifierNode).name);
283
+ } else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
284
+ isPrimitive = primitiveTypes.includes((typeArg as any).name);
285
+ }
286
+ }
287
+
288
+ if (!isPrimitive) {
289
+ this.diagnostics.push(
290
+ toDiagnostic(Errors.modelProviderRequired('ofType', node.range))
291
+ );
292
+ }
293
+ }
294
+
295
+ // Check ofType with union types
296
+ if (funcName === 'ofType' && node.typeInfo) {
297
+ const inputType = node.typeInfo;
298
+ if (node.arguments.length > 0 && inputType.modelContext &&
299
+ typeof inputType.modelContext === 'object' &&
300
+ 'isUnion' in inputType.modelContext &&
301
+ inputType.modelContext.isUnion &&
302
+ 'choices' in inputType.modelContext &&
303
+ Array.isArray(inputType.modelContext.choices)) {
304
+
305
+ // Extract target type from argument
306
+ let targetType: string | undefined;
307
+ const typeArg = node.arguments[0]!;
308
+ if (typeArg.type === NodeType.Identifier) {
309
+ targetType = (typeArg as IdentifierNode).name;
310
+ } else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
311
+ targetType = (typeArg as any).name;
312
+ }
313
+
314
+ if (targetType) {
315
+ const validChoice = inputType.modelContext.choices.find((choice: any) =>
316
+ choice.type === targetType || choice.code === targetType
317
+ );
318
+
319
+ if (!validChoice) {
320
+ this.diagnostics.push({
321
+ severity: DiagnosticSeverity.Warning,
322
+ code: 'invalid-type-filter',
323
+ message: `Type '${targetType}' is not present in the union type. Available types: ${
324
+ inputType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
325
+ }`,
326
+ range: node.range
327
+ });
328
+ }
329
+ }
330
+ }
331
+ }
332
+
170
333
  const func = registry.getFunction(funcName);
171
334
 
172
335
  if (!func) {
173
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown function: ${funcName}`, node, DiagnosticCode.UnknownFunction);
336
+ this.diagnostics.push(
337
+ toDiagnostic(Errors.unknownFunction(funcName, node.range))
338
+ );
174
339
  } else {
175
340
  // Check argument count based on signature
176
- const params = func.signature.parameters;
341
+ const params = func.signatures?.[0]?.parameters || [];
177
342
  const requiredParams = params.filter(p => !p.optional).length;
178
343
  const maxParams = params.length;
179
344
 
180
345
  if (node.arguments.length < requiredParams) {
181
- this.addDiagnostic(DiagnosticSeverity.Error, `Function '${funcName}' requires at least ${requiredParams} arguments, got ${node.arguments.length}`, node, DiagnosticCode.TooFewArgs);
346
+ this.diagnostics.push(
347
+ toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
348
+ );
182
349
  } else if (node.arguments.length > maxParams) {
183
- this.addDiagnostic(DiagnosticSeverity.Error, `Function '${funcName}' accepts at most ${maxParams} arguments, got ${node.arguments.length}`, node, DiagnosticCode.TooManyArgs);
350
+ this.diagnostics.push(
351
+ toDiagnostic(Errors.wrongArgumentCount(funcName, maxParams, node.arguments.length, node.range))
352
+ );
184
353
  }
185
354
 
186
355
  // Type check arguments if we have type information
@@ -193,32 +362,217 @@ export class Analyzer {
193
362
  node.arguments.forEach(arg => this.visitNode(arg));
194
363
  }
195
364
 
365
+ private visitMembershipTest(node: MembershipTestNode): void {
366
+ // Check if ModelProvider is required
367
+ // Basic primitive types can be checked without ModelProvider
368
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
369
+ if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
370
+ this.diagnostics.push(
371
+ toDiagnostic(Errors.modelProviderRequired('is', node.range))
372
+ );
373
+ }
374
+
375
+ // Check 'is' with union types
376
+ if (node.expression.typeInfo) {
377
+ const leftType = node.expression.typeInfo;
378
+ if (leftType.modelContext &&
379
+ typeof leftType.modelContext === 'object' &&
380
+ 'isUnion' in leftType.modelContext &&
381
+ leftType.modelContext.isUnion &&
382
+ 'choices' in leftType.modelContext &&
383
+ Array.isArray(leftType.modelContext.choices)) {
384
+
385
+ const targetTypeName = node.targetType;
386
+ const validChoice = leftType.modelContext.choices.find((choice: any) =>
387
+ choice.type === targetTypeName || choice.code === targetTypeName
388
+ );
389
+
390
+ if (!validChoice) {
391
+ this.diagnostics.push({
392
+ severity: DiagnosticSeverity.Warning,
393
+ code: 'invalid-type-test',
394
+ message: `Type test 'is ${targetTypeName}' will always be false. Type '${targetTypeName}' is not in the union. Available types: ${
395
+ leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
396
+ }`,
397
+ range: node.range
398
+ });
399
+ }
400
+ }
401
+ }
402
+
403
+ this.visitNode(node.expression);
404
+ }
405
+
406
+ private visitTypeCast(node: TypeCastNode): void {
407
+ // Check if ModelProvider is required
408
+ // Basic primitive types can be checked without ModelProvider
409
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
410
+ if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
411
+ this.diagnostics.push(
412
+ toDiagnostic(Errors.modelProviderRequired('as', node.range))
413
+ );
414
+ }
415
+
416
+ // Check 'as' with union types
417
+ if (node.expression.typeInfo) {
418
+ const leftType = node.expression.typeInfo;
419
+ if (leftType.modelContext &&
420
+ typeof leftType.modelContext === 'object' &&
421
+ 'isUnion' in leftType.modelContext &&
422
+ leftType.modelContext.isUnion &&
423
+ 'choices' in leftType.modelContext &&
424
+ Array.isArray(leftType.modelContext.choices)) {
425
+
426
+ const targetTypeName = node.targetType;
427
+ const validChoice = leftType.modelContext.choices.find((choice: any) =>
428
+ choice.type === targetTypeName || choice.code === targetTypeName
429
+ );
430
+
431
+ if (!validChoice) {
432
+ this.diagnostics.push({
433
+ severity: DiagnosticSeverity.Warning,
434
+ code: 'invalid-type-cast',
435
+ message: `Type cast 'as ${targetTypeName}' may fail. Type '${targetTypeName}' is not guaranteed in the union. Available types: ${
436
+ leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
437
+ }`,
438
+ range: node.range
439
+ });
440
+ }
441
+ }
442
+ }
443
+
444
+ this.visitNode(node.expression);
445
+ }
446
+
196
447
  // Unified variable validation to eliminate duplication
197
448
  private validateVariable(name: string, node: ASTNode): void {
198
449
  if (name.startsWith('$')) {
199
450
  if (!this.variables.has(name)) {
200
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown variable: ${name}`, node, DiagnosticCode.UnknownVariable);
451
+ this.diagnostics.push(
452
+ toDiagnostic(Errors.unknownVariable(name, node.range))
453
+ );
201
454
  }
202
455
  } else if (name.startsWith('%')) {
203
456
  const varName = name.substring(1);
204
457
  if (!this.variables.has(varName)) {
205
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown user variable: ${name}`, node, DiagnosticCode.UnknownUserVariable);
458
+ this.diagnostics.push(
459
+ toDiagnostic(Errors.unknownUserVariable(name, node.range))
460
+ );
206
461
  }
207
462
  }
208
463
  }
209
-
210
- private addDiagnostic(severity: DiagnosticSeverity, message: string, node: ASTNode, code: string): void {
211
- this.diagnostics.push({
212
- range: node.range,
213
- severity,
214
- message,
215
- code,
216
- source: 'fhirpath-analyzer'
217
- });
464
+
465
+ private collectDefinedVariables(node: ASTNode): Set<string> {
466
+ const vars = new Set<string>();
467
+
468
+ // If this is a defineVariable call, extract the variable name
469
+ if (node.type === NodeType.Function) {
470
+ const funcNode = node as FunctionNode;
471
+ if (funcNode.name.type === NodeType.Identifier &&
472
+ (funcNode.name as IdentifierNode).name === 'defineVariable' &&
473
+ funcNode.arguments.length >= 1) {
474
+ const nameArg = funcNode.arguments[0];
475
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
476
+ vars.add(nameArg.value as string);
477
+ }
478
+ }
479
+ }
480
+
481
+ // If this is a binary dot operator, collect from left side recursively
482
+ if (node.type === NodeType.Binary) {
483
+ const binaryNode = node as BinaryNode;
484
+ if (binaryNode.operator === '.') {
485
+ // Collect from left side
486
+ const leftVars = this.collectDefinedVariables(binaryNode.left);
487
+ leftVars.forEach(v => vars.add(v));
488
+
489
+ // Check if right side is also defineVariable
490
+ if (binaryNode.right.type === NodeType.Function) {
491
+ const rightFunc = binaryNode.right as FunctionNode;
492
+ if (rightFunc.name.type === NodeType.Identifier &&
493
+ (rightFunc.name as IdentifierNode).name === 'defineVariable' &&
494
+ rightFunc.arguments.length >= 1) {
495
+ const nameArg = rightFunc.arguments[0];
496
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
497
+ vars.add(nameArg.value as string);
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+
504
+ return vars;
505
+ }
506
+
507
+ private collectDefinedVariablesWithTypes(node: ASTNode): Map<string, TypeInfo> {
508
+ const varsWithTypes = new Map<string, TypeInfo>();
509
+
510
+ // If this is a defineVariable call, extract the variable name and type
511
+ if (node.type === NodeType.Function) {
512
+ const funcNode = node as FunctionNode;
513
+ if (funcNode.name.type === NodeType.Identifier &&
514
+ (funcNode.name as IdentifierNode).name === 'defineVariable' &&
515
+ funcNode.arguments.length >= 1) {
516
+ const nameArg = funcNode.arguments[0];
517
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
518
+ const varName = nameArg.value as string;
519
+ let varType: TypeInfo;
520
+
521
+ if (funcNode.arguments.length >= 2 && funcNode.arguments[1]!.typeInfo) {
522
+ // Has value expression - use its type
523
+ varType = funcNode.arguments[1]!.typeInfo;
524
+ } else if (node.typeInfo) {
525
+ // No value expression - uses input as value (defineVariable returns input)
526
+ varType = node.typeInfo;
527
+ } else {
528
+ varType = { type: 'Any', singleton: false };
529
+ }
530
+
531
+ varsWithTypes.set(varName, varType);
532
+ }
533
+ }
534
+ }
535
+
536
+ // If this is a binary dot operator, collect from entire chain
537
+ if (node.type === NodeType.Binary) {
538
+ const binaryNode = node as BinaryNode;
539
+ if (binaryNode.operator === '.') {
540
+ // Collect from left side recursively
541
+ const leftVars = this.collectDefinedVariablesWithTypes(binaryNode.left);
542
+ leftVars.forEach((type, name) => varsWithTypes.set(name, type));
543
+
544
+ // Check if right side is also defineVariable
545
+ if (binaryNode.right.type === NodeType.Function) {
546
+ const rightFunc = binaryNode.right as FunctionNode;
547
+ if (rightFunc.name.type === NodeType.Identifier &&
548
+ (rightFunc.name as IdentifierNode).name === 'defineVariable' &&
549
+ rightFunc.arguments.length >= 1) {
550
+ const nameArg = rightFunc.arguments[0];
551
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
552
+ const varName = nameArg.value as string;
553
+ let varType: TypeInfo;
554
+
555
+ if (rightFunc.arguments.length >= 2 && rightFunc.arguments[1]!.typeInfo) {
556
+ varType = rightFunc.arguments[1]!.typeInfo;
557
+ } else if (binaryNode.typeInfo) {
558
+ varType = binaryNode.typeInfo;
559
+ } else {
560
+ varType = { type: 'Any', singleton: false };
561
+ }
562
+
563
+ varsWithTypes.set(varName, varType);
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ return varsWithTypes;
218
571
  }
219
572
 
573
+
220
574
  // Type inference methods
221
- private inferType(node: ASTNode, inputType?: TypeInfo): TypeInfo {
575
+ private async inferType(node: ASTNode, inputType?: TypeInfo): Promise<TypeInfo> {
222
576
  // Handle error nodes
223
577
  if (node.type === 'Error') {
224
578
  return this.inferErrorNodeType(node as ErrorNode, inputType);
@@ -229,31 +583,31 @@ export class Analyzer {
229
583
  return this.inferLiteralType(node as LiteralNode);
230
584
 
231
585
  case NodeType.Binary:
232
- return this.inferBinaryType(node as BinaryNode, inputType);
586
+ return await this.inferBinaryType(node as BinaryNode, inputType);
233
587
 
234
588
  case NodeType.Unary:
235
589
  return this.inferUnaryType(node as UnaryNode);
236
590
 
237
591
  case NodeType.Function:
238
- return this.inferFunctionType(node as FunctionNode, inputType);
592
+ return await this.inferFunctionType(node as FunctionNode, inputType);
239
593
 
240
594
  case NodeType.Identifier:
241
- return this.inferIdentifierType(node as IdentifierNode, inputType);
595
+ return await this.inferIdentifierType(node as IdentifierNode, inputType);
242
596
 
243
597
  case NodeType.Variable:
244
598
  return this.inferVariableType(node as VariableNode);
245
599
 
246
600
  case NodeType.Collection:
247
- return this.inferCollectionType(node as CollectionNode);
601
+ return await this.inferCollectionType(node as CollectionNode);
248
602
 
249
603
  case NodeType.TypeCast:
250
- return this.inferTypeCastType(node as TypeCastNode);
604
+ return await this.inferTypeCastType(node as TypeCastNode);
251
605
 
252
606
  case NodeType.MembershipTest:
253
607
  return { type: 'Boolean', singleton: true };
254
608
 
255
609
  case NodeType.TypeOrIdentifier:
256
- return this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
610
+ return await this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
257
611
 
258
612
  default:
259
613
  return { type: 'Any', singleton: false };
@@ -291,7 +645,7 @@ export class Analyzer {
291
645
  }
292
646
  }
293
647
 
294
- private inferBinaryType(node: BinaryNode, inputType?: TypeInfo): TypeInfo {
648
+ private async inferBinaryType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
295
649
  const operator = registry.getOperatorDefinition(node.operator);
296
650
  if (!operator) {
297
651
  return { type: 'Any', singleton: false };
@@ -299,12 +653,12 @@ export class Analyzer {
299
653
 
300
654
  // For navigation (dot operator), we need special handling
301
655
  if (node.operator === '.') {
302
- return this.inferNavigationType(node, inputType);
656
+ return await this.inferNavigationType(node, inputType);
303
657
  }
304
658
 
305
659
  // Infer types of operands
306
- const leftType = this.inferType(node.left, inputType);
307
- const rightType = this.inferType(node.right, inputType);
660
+ const leftType = await this.inferType(node.left, inputType);
661
+ const rightType = await this.inferType(node.right, inputType);
308
662
 
309
663
  // Find matching signature
310
664
  for (const sig of operator.signatures) {
@@ -319,12 +673,12 @@ export class Analyzer {
319
673
  return this.resolveResultType(defaultResult, inputType, leftType, rightType);
320
674
  }
321
675
 
322
- private inferNavigationType(node: BinaryNode, inputType?: TypeInfo): TypeInfo {
323
- const leftType = this.inferType(node.left, inputType);
676
+ private async inferNavigationType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
677
+ const leftType = await this.inferType(node.left, inputType);
324
678
 
325
679
  // If the right side is a function, return the function's type
326
680
  if (node.right.type === NodeType.Function) {
327
- return this.inferType(node.right, leftType);
681
+ return await this.inferType(node.right, leftType);
328
682
  }
329
683
 
330
684
  // If we have a model provider and the right side is an identifier
@@ -332,7 +686,7 @@ export class Analyzer {
332
686
  const propertyName = (node.right as IdentifierNode).name;
333
687
 
334
688
  // Use getElementType to navigate the property
335
- const resultType = this.modelProvider.getElementType(leftType, propertyName);
689
+ const resultType = await this.modelProvider.getElementType(leftType, propertyName);
336
690
  if (resultType) {
337
691
  return resultType;
338
692
  }
@@ -341,11 +695,8 @@ export class Analyzer {
341
695
  // Skip diagnostics for union types - they may have dynamic properties
342
696
  if (leftType.namespace && leftType.name && leftType.modelContext &&
343
697
  !(leftType.modelContext as any).isUnion) {
344
- this.addDiagnostic(
345
- DiagnosticSeverity.Error,
346
- `Unknown property '${propertyName}' on type ${leftType.namespace}.${leftType.name}`,
347
- node.right,
348
- 'UNKNOWN_PROPERTY'
698
+ this.diagnostics.push(
699
+ toDiagnostic(Errors.unknownProperty(propertyName, `${leftType.namespace}.${leftType.name}`, node.right.range))
349
700
  );
350
701
  }
351
702
  }
@@ -369,7 +720,7 @@ export class Analyzer {
369
720
  return { type: 'Any', singleton: false };
370
721
  }
371
722
 
372
- private inferFunctionType(node: FunctionNode, inputType?: TypeInfo): TypeInfo {
723
+ private async inferFunctionType(node: FunctionNode, inputType?: TypeInfo): Promise<TypeInfo> {
373
724
  if (node.name.type !== NodeType.Identifier) {
374
725
  return { type: 'Any', singleton: false };
375
726
  }
@@ -381,28 +732,112 @@ export class Analyzer {
381
732
  return { type: 'Any', singleton: false };
382
733
  }
383
734
 
735
+ // Special handling for iif function
736
+ if (funcName === 'iif') {
737
+ // iif returns the common type of the true and false branches
738
+ if (node.arguments.length >= 2) {
739
+ const trueBranchType = await this.inferType(node.arguments[1]!, inputType);
740
+ if (node.arguments.length >= 3) {
741
+ const falseBranchType = await this.inferType(node.arguments[2]!, inputType);
742
+ // If both branches have the same type, use that
743
+ if (trueBranchType.type === falseBranchType.type &&
744
+ trueBranchType.singleton === falseBranchType.singleton) {
745
+ return trueBranchType;
746
+ }
747
+ // If types are the same but singleton differs, return as collection
748
+ if (trueBranchType.type === falseBranchType.type) {
749
+ // One is singleton, one is collection - result must be collection
750
+ return { type: trueBranchType.type, singleton: false };
751
+ }
752
+ // Otherwise, check if one is a subtype of the other
753
+ if (this.isTypeCompatible(trueBranchType, falseBranchType)) {
754
+ return falseBranchType;
755
+ }
756
+ if (this.isTypeCompatible(falseBranchType, trueBranchType)) {
757
+ return trueBranchType;
758
+ }
759
+ } else {
760
+ // Only true branch, result can be that type or empty
761
+ return { ...trueBranchType, singleton: false };
762
+ }
763
+ }
764
+ return { type: 'Any', singleton: false };
765
+ }
766
+
767
+ // Special handling for defineVariable function
768
+ if (funcName === 'defineVariable') {
769
+ // defineVariable returns its input type unchanged
770
+ return inputType || { type: 'Any', singleton: false };
771
+ }
772
+
773
+ // Special handling for aggregate function
774
+ if (funcName === 'aggregate') {
775
+ // If init parameter is provided, use its type to infer result type
776
+ if (node.arguments.length >= 2) {
777
+ const initType = await this.inferType(node.arguments[1]!, inputType);
778
+ // The result type is the same as init type
779
+ return initType;
780
+ }
781
+ // Without init, we can't fully infer the type without running annotation
782
+ // This is a limitation - the actual type will be set during annotateAST
783
+ if (node.arguments.length >= 1) {
784
+ // We could try to infer, but it would require setting up system variables
785
+ // For now, return Any and let annotateAST handle proper typing
786
+ return { type: 'Any', singleton: false };
787
+ }
788
+ // No arguments at all
789
+ return { type: 'Any', singleton: false };
790
+ }
791
+
792
+ // Special handling for children function
793
+ if (funcName === 'children') {
794
+ if (inputType && this.modelProvider && 'getChildrenType' in this.modelProvider) {
795
+ const childrenType = await this.modelProvider.getChildrenType(inputType);
796
+ if (childrenType) {
797
+ return childrenType;
798
+ }
799
+ }
800
+ // Fallback to Any collection
801
+ return { type: 'Any', singleton: false };
802
+ }
803
+
804
+ // Special handling for descendants function
805
+ // Returns Any type due to combinatorial explosion of possible types
806
+ if (funcName === 'descendants') {
807
+ return { type: 'Any', singleton: false };
808
+ }
809
+
384
810
  // Special handling for functions with dynamic result types
385
- if (func.signature.result === 'inputType') {
811
+ // Use first matching signature's result type
812
+ const matchingSignature = func.signatures?.find(sig =>
813
+ !sig.input || !inputType || this.isTypeCompatible(inputType, sig.input)
814
+ ) || func.signatures?.[0];
815
+
816
+ if (!matchingSignature) {
817
+ return { type: 'Any', singleton: false };
818
+ }
819
+
820
+ if (matchingSignature.result === 'inputType') {
386
821
  // Functions like where() return the same type as input but always as collection
387
822
  return inputType ? { ...inputType, singleton: false } : { type: 'Any', singleton: false };
388
- } else if (func.signature.result === 'inputTypeSingleton') {
823
+ } else if (matchingSignature.result === 'inputTypeSingleton') {
389
824
  // Functions like first(), last() return the same type as input but as singleton
390
825
  return inputType ? { ...inputType, singleton: true } : { type: 'Any', singleton: true };
391
- } else if (func.signature.result === 'parameterType' && node.arguments.length > 0) {
826
+ } else if (matchingSignature.result === 'parameterType' && node.arguments.length > 0) {
392
827
  // Functions like select() return the type of the first parameter expression as collection
393
- const paramType = this.inferType(node.arguments[0]!, inputType);
828
+ const paramType = await this.inferType(node.arguments[0]!, inputType);
394
829
  return { ...paramType, singleton: false };
395
- } else if (typeof func.signature.result === 'object') {
396
- return func.signature.result;
830
+ } else if (typeof matchingSignature.result === 'object') {
831
+ return matchingSignature.result;
397
832
  }
398
833
 
399
834
  return { type: 'Any', singleton: false };
400
835
  }
401
836
 
402
- private inferIdentifierType(node: IdentifierNode, inputType?: TypeInfo): TypeInfo {
837
+ private async inferIdentifierType(node: IdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
403
838
  // First, try to navigate from input type (most common case)
404
839
  if (inputType && this.modelProvider) {
405
- const elementType = this.modelProvider.getElementType(inputType, node.name);
840
+ const elementType = await this.modelProvider.getElementType(inputType, node.name);
406
841
  if (elementType) {
407
842
  return elementType;
408
843
  }
@@ -410,8 +845,11 @@ export class Analyzer {
410
845
 
411
846
  // Only check if it's a type name if it starts with uppercase (FHIR convention)
412
847
  // or if there's no input type context
413
- if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name))) {
414
- const typeInfo = this.modelProvider.getType(node.name);
848
+ // Skip common FHIRPath keywords and function names that aren't types
849
+ const fhirPathKeywords = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity', 'ofType'];
850
+ if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name)) && !fhirPathKeywords.includes(node.name)) {
851
+ // Try to get type from model provider
852
+ const typeInfo = await this.modelProvider.getType(node.name);
415
853
  if (typeInfo) {
416
854
  return typeInfo;
417
855
  }
@@ -420,20 +858,23 @@ export class Analyzer {
420
858
  return { type: 'Any', singleton: false };
421
859
  }
422
860
 
423
- private inferTypeOrIdentifierType(node: TypeOrIdentifierNode, inputType?: TypeInfo): TypeInfo {
861
+ private async inferTypeOrIdentifierType(node: TypeOrIdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
424
862
  // TypeOrIdentifier can be either a type name or a property navigation
425
863
 
426
864
  // First, try navigation from input type (most common case)
427
865
  if (inputType && this.modelProvider) {
428
- const elementType = this.modelProvider.getElementType(inputType, node.name);
866
+ const elementType = await this.modelProvider.getElementType(inputType, node.name);
429
867
  if (elementType) {
430
868
  return elementType;
431
869
  }
432
870
  }
433
871
 
434
872
  // Then check if it's a type name (only for uppercase names or no input context)
435
- if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name))) {
436
- const typeInfo = this.modelProvider.getType(node.name);
873
+ // Skip common FHIRPath keywords and function names that aren't types
874
+ const fhirPathKeywords = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity', 'ofType'];
875
+ if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name)) && !fhirPathKeywords.includes(node.name)) {
876
+ // Try to get type from model provider
877
+ const typeInfo = await this.modelProvider.getType(node.name);
437
878
  if (typeInfo) {
438
879
  return typeInfo;
439
880
  }
@@ -443,11 +884,28 @@ export class Analyzer {
443
884
  }
444
885
 
445
886
  private inferVariableType(node: VariableNode): TypeInfo {
446
- // Built-in variables
447
- if (node.name === '$this') {
448
- return { type: 'Any', singleton: false }; // $this is usually a collection
449
- } else if (node.name === '$index' || node.name === '$total') {
450
- return { type: 'Integer', singleton: true };
887
+ // System variables - check temporary context
888
+ if (node.name.startsWith('$')) {
889
+ const systemType = this.systemVariableTypes.get(node.name);
890
+ if (systemType) {
891
+ return systemType;
892
+ }
893
+ // Fallback defaults for system variables
894
+ switch (node.name) {
895
+ case '$this':
896
+ return { type: 'Any', singleton: false };
897
+ case '$index':
898
+ return { type: 'Integer', singleton: true };
899
+ case '$total':
900
+ return { type: 'Any', singleton: false };
901
+ default:
902
+ return { type: 'Any', singleton: false };
903
+ }
904
+ }
905
+
906
+ // Special FHIRPath environment variables
907
+ if (node.name === '%context' || node.name === '%resource' || node.name === '%rootResource') {
908
+ return { type: 'Any', singleton: false }; // These return the original input
451
909
  }
452
910
 
453
911
  // User-defined variables - check with or without % prefix
@@ -464,13 +922,13 @@ export class Analyzer {
464
922
  return { type: 'Any', singleton: true };
465
923
  }
466
924
 
467
- private inferCollectionType(node: CollectionNode): TypeInfo {
925
+ private async inferCollectionType(node: CollectionNode): Promise<TypeInfo> {
468
926
  if (node.elements.length === 0) {
469
927
  return { type: 'Any', singleton: false };
470
928
  }
471
929
 
472
930
  // Infer types of all elements
473
- const elementTypes = node.elements.map(el => this.inferType(el));
931
+ const elementTypes = await Promise.all(node.elements.map(el => this.inferType(el)));
474
932
 
475
933
  // If all elements have the same type, use that
476
934
  const firstType = elementTypes[0];
@@ -490,12 +948,12 @@ export class Analyzer {
490
948
  return { type: 'Any', singleton: false };
491
949
  }
492
950
 
493
- private inferTypeCastType(node: TypeCastNode): TypeInfo {
951
+ private async inferTypeCastType(node: TypeCastNode): Promise<TypeInfo> {
494
952
  const targetType = node.targetType;
495
953
 
496
- // If we have a model provider, try to get the type info
954
+ // If we have a model provider, try to get the type
497
955
  if (this.modelProvider) {
498
- const typeInfo = this.modelProvider.getType(targetType);
956
+ const typeInfo = await this.modelProvider.getType(targetType);
499
957
  if (typeInfo) {
500
958
  return typeInfo;
501
959
  }
@@ -626,17 +1084,14 @@ export class Analyzer {
626
1084
  if (!foundMatch) {
627
1085
  const leftTypeStr = this.typeToString(leftType);
628
1086
  const rightTypeStr = this.typeToString(rightType);
629
- this.addDiagnostic(
630
- DiagnosticSeverity.Error,
631
- `Type mismatch: operator '${node.operator}' cannot be applied to types ${leftTypeStr} and ${rightTypeStr}`,
632
- node,
633
- DiagnosticCode.TypeMismatch
1087
+ this.diagnostics.push(
1088
+ toDiagnostic(Errors.operatorTypeMismatch(node.operator, leftTypeStr, rightTypeStr, node.range))
634
1089
  );
635
1090
  }
636
1091
  }
637
1092
 
638
1093
  private checkFunctionArgumentTypes(node: FunctionNode, func: import('./types').FunctionDefinition): void {
639
- const params = func.signature.parameters;
1094
+ const params = func.signatures?.[0]?.parameters || [];
640
1095
 
641
1096
  for (let i = 0; i < Math.min(node.arguments.length, params.length); i++) {
642
1097
  const arg = node.arguments[i]!;
@@ -647,11 +1102,8 @@ export class Analyzer {
647
1102
  if (!this.isTypeCompatible(arg.typeInfo, param.type)) {
648
1103
  const argTypeStr = this.typeToString(arg.typeInfo);
649
1104
  const paramTypeStr = this.typeToString(param.type);
650
- this.addDiagnostic(
651
- DiagnosticSeverity.Error,
652
- `Type mismatch: argument ${i + 1} of function '${func.name}' expects ${paramTypeStr} but got ${argTypeStr}`,
653
- arg,
654
- DiagnosticCode.ArgumentTypeMismatch
1105
+ this.diagnostics.push(
1106
+ toDiagnostic(Errors.argumentTypeMismatch(i + 1, func.name, paramTypeStr, argTypeStr, arg.range))
655
1107
  );
656
1108
  }
657
1109
  }
@@ -666,72 +1118,279 @@ export class Analyzer {
666
1118
  return `${type.type}${singletonStr}`;
667
1119
  }
668
1120
 
1121
+ /**
1122
+ * Infer the expected type at a cursor position based on context
1123
+ */
1124
+ private inferExpectedTypeForCursor(cursorNode: AnyCursorNode, inputType?: TypeInfo): TypeInfo | undefined {
1125
+ const context = cursorNode.context;
1126
+
1127
+ switch (context) {
1128
+ case CursorContext.Identifier:
1129
+ // After dot, expecting a member of the input type
1130
+ return inputType;
1131
+
1132
+ case CursorContext.Type:
1133
+ // After is/as/ofType, expecting a type name
1134
+ return { type: 'System.String' as TypeName, singleton: true };
1135
+
1136
+ case CursorContext.Argument:
1137
+ // In function argument, would need to look up function signature
1138
+ // For now, return Any
1139
+ return { type: 'Any' as TypeName, singleton: false };
1140
+
1141
+ case CursorContext.Index:
1142
+ // In indexer, expecting Integer
1143
+ return { type: 'Integer' as TypeName, singleton: true };
1144
+
1145
+ case CursorContext.Operator:
1146
+ // Between expressions, could be any operator
1147
+ // Return the input type as context
1148
+ return inputType;
1149
+
1150
+ default:
1151
+ return undefined;
1152
+ }
1153
+ }
1154
+
669
1155
  /**
670
1156
  * Annotate AST with type information
671
1157
  */
672
- private annotateAST(node: ASTNode, inputType?: TypeInfo): void {
1158
+ private async annotateAST(node: ASTNode, inputType?: TypeInfo): Promise<void> {
1159
+ // Check for cursor node in cursor mode
1160
+ if (this.cursorMode && isCursorNode(node)) {
1161
+ this.stoppedAtCursor = true;
1162
+ this.cursorContext = {
1163
+ cursorNode: node as AnyCursorNode,
1164
+ typeBeforeCursor: inputType,
1165
+ expectedType: this.inferExpectedTypeForCursor(node as AnyCursorNode, inputType)
1166
+ };
1167
+ // Still attach a type to the cursor node for consistency
1168
+ (node as any).typeInfo = inputType || { type: 'Any', singleton: false };
1169
+ return; // Short-circuit
1170
+ }
1171
+
1172
+ // If we've already stopped at cursor, don't continue
1173
+ if (this.stoppedAtCursor) {
1174
+ return;
1175
+ }
1176
+
673
1177
  // Handle error nodes
674
1178
  if (node.type === 'Error') {
675
1179
  const errorNode = node as ErrorNode;
676
1180
  // Infer a reasonable type for error nodes
677
1181
  node.typeInfo = this.inferErrorNodeType(errorNode, inputType);
678
1182
  // Add diagnostic for the error
679
- this.addDiagnostic(
680
- errorNode.severity || DiagnosticSeverity.Error,
681
- errorNode.message,
682
- errorNode,
683
- errorNode.code?.toString() || DiagnosticCode.ParseError
684
- );
1183
+ this.diagnostics.push({
1184
+ severity: errorNode.severity || DiagnosticSeverity.Error,
1185
+ message: errorNode.message,
1186
+ range: errorNode.range,
1187
+ code: errorNode.code?.toString() || 'FP5003',
1188
+ source: 'fhirpath'
1189
+ });
685
1190
  return;
686
1191
  }
687
1192
 
688
1193
  // Infer and attach type info
689
- node.typeInfo = this.inferType(node, inputType);
1194
+ node.typeInfo = await this.inferType(node, inputType);
690
1195
 
691
1196
  // Recursively annotate children
692
1197
  switch (node.type) {
693
1198
  case NodeType.Binary:
694
1199
  const binaryNode = node as BinaryNode;
695
- this.annotateAST(binaryNode.left, inputType);
1200
+ await this.annotateAST(binaryNode.left, inputType);
1201
+
1202
+ // If we stopped at cursor, don't continue
1203
+ if (this.stoppedAtCursor) {
1204
+ break;
1205
+ }
1206
+
1207
+ // Check if right side is a cursor node - if so, set type from left
1208
+ if (this.cursorMode && isCursorNode(binaryNode.right)) {
1209
+ this.stoppedAtCursor = true;
1210
+ this.cursorContext = {
1211
+ cursorNode: binaryNode.right as AnyCursorNode,
1212
+ typeBeforeCursor: binaryNode.left.typeInfo,
1213
+ expectedType: this.inferExpectedTypeForCursor(binaryNode.right as AnyCursorNode, binaryNode.left.typeInfo)
1214
+ };
1215
+ // Still attach type to cursor node
1216
+ (binaryNode.right as any).typeInfo = binaryNode.left.typeInfo || { type: 'Any', singleton: false };
1217
+ break;
1218
+ }
696
1219
 
697
1220
  // For navigation, pass the left's type as input to the right
698
1221
  if (binaryNode.operator === '.') {
699
- this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1222
+ // Collect all variables defined in the left side chain
1223
+ const definedVarsWithTypes = this.collectDefinedVariablesWithTypes(binaryNode.left);
1224
+
1225
+ if (definedVarsWithTypes.size > 0) {
1226
+ // Save current variable types
1227
+ const savedTypes = new Map<string, TypeInfo>();
1228
+ definedVarsWithTypes.forEach((type, varName) => {
1229
+ const currentType = this.userVariableTypes.get(varName);
1230
+ if (currentType) {
1231
+ savedTypes.set(varName, currentType);
1232
+ }
1233
+ this.userVariableTypes.set(varName, type);
1234
+ });
1235
+
1236
+ // Annotate right side with new variables in scope
1237
+ await this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1238
+
1239
+ // Restore previous types
1240
+ definedVarsWithTypes.forEach((_, varName) => {
1241
+ const savedType = savedTypes.get(varName);
1242
+ if (savedType) {
1243
+ this.userVariableTypes.set(varName, savedType);
1244
+ } else {
1245
+ this.userVariableTypes.delete(varName);
1246
+ }
1247
+ });
1248
+ } else {
1249
+ // No defineVariable in chain, proceed normally
1250
+ await this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1251
+ }
700
1252
  } else {
701
- this.annotateAST(binaryNode.right, inputType);
1253
+ await this.annotateAST(binaryNode.right, inputType);
702
1254
  }
703
1255
  break;
704
1256
 
705
1257
  case NodeType.Unary:
706
1258
  const unaryNode = node as UnaryNode;
707
- this.annotateAST(unaryNode.operand, inputType);
1259
+ await this.annotateAST(unaryNode.operand, inputType);
708
1260
  break;
709
1261
 
710
1262
  case NodeType.Function:
711
1263
  const funcNode = node as FunctionNode;
712
- this.annotateAST(funcNode.name, inputType);
713
- funcNode.arguments.forEach(arg => this.annotateAST(arg, inputType));
1264
+ await this.annotateAST(funcNode.name, inputType);
1265
+
1266
+ // Special handling for aggregate function arguments
1267
+ if (funcNode.name.type === NodeType.Identifier &&
1268
+ (funcNode.name as IdentifierNode).name === 'aggregate') {
1269
+ // Aggregate establishes both $this and $total
1270
+ if (funcNode.arguments.length >= 1) {
1271
+ const itemType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
1272
+
1273
+ // Save current system variable context
1274
+ const savedThis = this.systemVariableTypes.get('$this');
1275
+ const savedTotal = this.systemVariableTypes.get('$total');
1276
+
1277
+ // Set $this for iteration
1278
+ this.systemVariableTypes.set('$this', itemType);
1279
+
1280
+ if (funcNode.arguments.length >= 2) {
1281
+ // Has init parameter - evaluate it first
1282
+ await this.annotateAST(funcNode.arguments[1]!, inputType);
1283
+ const initType = funcNode.arguments[1]!.typeInfo;
1284
+
1285
+ // Set $total to init type
1286
+ if (initType) {
1287
+ this.systemVariableTypes.set('$total', initType);
1288
+ } else {
1289
+ this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
1290
+ }
1291
+
1292
+ // Process aggregator with both variables set
1293
+ await this.annotateAST(funcNode.arguments[0]!, inputType);
1294
+
1295
+ // Process remaining arguments
1296
+ for (const arg of funcNode.arguments.slice(2)) {
1297
+ await this.annotateAST(arg, inputType);
1298
+ if (this.stoppedAtCursor) break;
1299
+ }
1300
+ } else {
1301
+ // No init - first pass to infer aggregator type
1302
+ this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
1303
+ await this.annotateAST(funcNode.arguments[0]!, inputType);
1304
+
1305
+ // Second pass with inferred type
1306
+ const aggregatorType = funcNode.arguments[0]!.typeInfo;
1307
+ if (aggregatorType) {
1308
+ this.systemVariableTypes.set('$total', aggregatorType);
1309
+ // Re-annotate with proper $total type
1310
+ await this.annotateAST(funcNode.arguments[0]!, inputType);
1311
+ }
1312
+ }
1313
+
1314
+ // Restore previous context
1315
+ if (savedThis) {
1316
+ this.systemVariableTypes.set('$this', savedThis);
1317
+ } else {
1318
+ this.systemVariableTypes.delete('$this');
1319
+ }
1320
+ if (savedTotal) {
1321
+ this.systemVariableTypes.set('$total', savedTotal);
1322
+ } else {
1323
+ this.systemVariableTypes.delete('$total');
1324
+ }
1325
+ }
1326
+ } else {
1327
+ // Special handling for functions that pass their input as context to arguments
1328
+ const funcName = funcNode.name.type === NodeType.Identifier ?
1329
+ (funcNode.name as IdentifierNode).name : null;
1330
+
1331
+ if (funcName && ['where', 'select', 'all', 'exists'].includes(funcName)) {
1332
+ // These functions establish $this as each element of the input collection
1333
+ const elementType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
1334
+
1335
+ // Save current system variable context
1336
+ const savedThis = this.systemVariableTypes.get('$this');
1337
+ const savedIndex = this.systemVariableTypes.get('$index');
1338
+
1339
+ // Set system variables for expression evaluation
1340
+ this.systemVariableTypes.set('$this', elementType);
1341
+ this.systemVariableTypes.set('$index', { type: 'Integer', singleton: true });
1342
+
1343
+ // Process arguments with system variables in scope
1344
+ for (const arg of funcNode.arguments) {
1345
+ await this.annotateAST(arg, inputType);
1346
+ if (this.stoppedAtCursor) break;
1347
+ }
1348
+
1349
+ // Restore previous context
1350
+ if (savedThis) {
1351
+ this.systemVariableTypes.set('$this', savedThis);
1352
+ } else {
1353
+ this.systemVariableTypes.delete('$this');
1354
+ }
1355
+ if (savedIndex) {
1356
+ this.systemVariableTypes.set('$index', savedIndex);
1357
+ } else {
1358
+ this.systemVariableTypes.delete('$index');
1359
+ }
1360
+ } else {
1361
+ // Regular function argument annotation
1362
+ for (const arg of funcNode.arguments) {
1363
+ await this.annotateAST(arg, inputType);
1364
+ if (this.stoppedAtCursor) break;
1365
+ }
1366
+ }
1367
+ }
714
1368
  break;
715
1369
 
716
1370
  case NodeType.Collection:
717
1371
  const collNode = node as CollectionNode;
718
- collNode.elements.forEach(el => this.annotateAST(el, inputType));
1372
+ for (const el of collNode.elements) {
1373
+ await this.annotateAST(el, inputType);
1374
+ if (this.stoppedAtCursor) break;
1375
+ }
719
1376
  break;
720
1377
 
721
1378
  case NodeType.TypeCast:
722
1379
  const castNode = node as TypeCastNode;
723
- this.annotateAST(castNode.expression, inputType);
1380
+ await this.annotateAST(castNode.expression, inputType);
724
1381
  break;
725
1382
 
726
1383
  case NodeType.MembershipTest:
727
1384
  const memberNode = node as MembershipTestNode;
728
- this.annotateAST(memberNode.expression, inputType);
1385
+ await this.annotateAST(memberNode.expression, inputType);
729
1386
  break;
730
1387
 
731
1388
  case NodeType.Index:
732
1389
  const indexNode = node as IndexNode;
733
- this.annotateAST(indexNode.expression, inputType);
734
- this.annotateAST(indexNode.index, inputType);
1390
+ await this.annotateAST(indexNode.expression, inputType);
1391
+ if (!this.stoppedAtCursor) {
1392
+ await this.annotateAST(indexNode.index, inputType);
1393
+ }
735
1394
  break;
736
1395
 
737
1396
  case NodeType.TypeOrIdentifier: