@atomic-ehr/fhirpath 0.0.2 → 0.0.3

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 (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
package/src/analyzer.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  BinaryNode,
4
4
  IdentifierNode,
5
5
  LiteralNode,
6
+ TemporalLiteralNode,
6
7
  FunctionNode,
7
8
  Diagnostic,
8
9
  AnalysisResult,
@@ -15,15 +16,20 @@ import type {
15
16
  ModelProvider,
16
17
  VariableNode,
17
18
  TypeName,
18
- TypeOrIdentifierNode,
19
- ErrorNode
19
+ ErrorNode,
20
+ InternalAnalysisResult,
21
+ QuantityNode
20
22
  } from './types';
21
- import { NodeType, DiagnosticSeverity } from './types';
23
+ import { NodeType, DiagnosticSeverity, AnalysisContext } from './types';
24
+ import type { OperatorSignature, FunctionSignature } from './types';
25
+ import type { FunctionDefinition } from './types';
22
26
  import { registry } from './registry';
23
- import { Errors, toDiagnostic } from './errors';
24
- import { isCursorNode, CursorContext } from './cursor-nodes';
25
- import type { AnyCursorNode } from './cursor-nodes';
26
-
27
+ import { matchOperatorSignature, matchFunctionSignature, resolveResultType } from './analyzer/type-compat';
28
+ import { checkParamTypes, formatType, isEmptyCollection, isUnionType, getUnionChoices, validateUnionChoice } from './analyzer/utils';
29
+ import { Errors, toDiagnostic, ErrorCodes } from './errors';
30
+ import { isCursorNode, CursorContext } from './parser/cursor-nodes';
31
+ import type { AnyCursorNode } from './parser/cursor-nodes';
32
+ import { Parser, type ParserOptions } from './parser';
27
33
 
28
34
  export interface AnalyzerOptions {
29
35
  cursorMode?: boolean;
@@ -35,992 +41,1001 @@ export interface AnalysisResultWithCursor extends AnalysisResult {
35
41
  typeBeforeCursor?: TypeInfo;
36
42
  expectedType?: TypeInfo;
37
43
  cursorNode?: AnyCursorNode;
44
+ functionCall?: {
45
+ definition: import('./types').FunctionDefinition;
46
+ argumentIndex: number;
47
+ };
38
48
  };
39
49
  }
40
50
 
41
51
  export class Analyzer {
42
- private diagnostics: Diagnostic[] = [];
43
- private variables: Set<string> = new Set(['$this', '$index', '$total', 'context', 'resource', 'rootResource']);
44
52
  private modelProvider?: ModelProvider;
45
- private userVariableTypes: Map<string, TypeInfo> = new Map();
46
- private systemVariableTypes: Map<string, TypeInfo> = new Map();
47
53
  private cursorMode: boolean = false;
48
54
  private stoppedAtCursor: boolean = false;
49
55
  private cursorContext?: {
50
56
  typeBeforeCursor?: TypeInfo;
51
57
  expectedType?: TypeInfo;
52
58
  cursorNode?: AnyCursorNode;
59
+ functionCall?: {
60
+ definition: import('./types').FunctionDefinition;
61
+ argumentIndex: number;
62
+ };
53
63
  };
54
64
 
55
65
  constructor(modelProvider?: ModelProvider) {
56
66
  this.modelProvider = modelProvider;
57
67
  }
58
68
 
59
- async analyze(
60
- ast: ASTNode,
61
- userVariables?: Record<string, any>,
62
- inputType?: TypeInfo,
63
- options?: AnalyzerOptions
69
+ /**
70
+ * Parse and analyze a FHIRPath expression end-to-end, returning an AnalysisResult
71
+ * and structured diagnostics. Supports optional error recovery (LSP mode).
72
+ */
73
+ static async analyzeExpression(
74
+ expression: string,
75
+ options: {
76
+ variables?: Record<string, unknown>;
77
+ modelProvider?: ModelProvider;
78
+ inputType?: TypeInfo;
79
+ errorRecovery?: boolean;
80
+ parserOptions?: ParserOptions;
81
+ } = {}
64
82
  ): Promise<AnalysisResultWithCursor> {
65
- this.diagnostics = [];
66
- this.userVariableTypes.clear();
67
- this.cursorMode = options?.cursorMode ?? false;
68
- this.stoppedAtCursor = false;
69
- this.cursorContext = undefined;
70
-
71
- if (userVariables) {
72
- Object.keys(userVariables).forEach(name => {
73
- this.variables.add(name);
74
- // Try to infer types from values
75
- const value = userVariables[name];
76
- if (value !== undefined && value !== null) {
77
- this.userVariableTypes.set(name, this.inferValueType(value));
78
- }
79
- });
80
- }
81
-
82
- // Annotate AST with type information
83
- await this.annotateAST(ast, inputType);
84
-
85
- // Perform validation with type checking (if not stopped at cursor)
86
- if (!this.stoppedAtCursor) {
87
- this.visitNode(ast);
83
+ const parserOptions: ParserOptions | undefined = options.errorRecovery
84
+ ? { mode: 'lsp', errorRecovery: true, ...(options.parserOptions || {}) }
85
+ : options.parserOptions;
86
+
87
+ const parser = new Parser(expression, parserOptions);
88
+ const parseResult = parser.parse();
89
+
90
+ // If error recovery is not enabled and there are parse errors, surface the first one.
91
+ if (!options.errorRecovery && parseResult.errors.length > 0) {
92
+ // Preserve backward compatibility; analyzer doesn't throw custom mapping here.
93
+ throw Errors.invalidSyntax(parseResult.errors[0]!.message);
88
94
  }
89
-
90
- return {
91
- diagnostics: this.diagnostics,
92
- ast,
93
- stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
94
- cursorContext: this.cursorMode ? this.cursorContext : undefined
95
- };
95
+
96
+ const analyzer = new Analyzer(options.modelProvider);
97
+ const result = await analyzer.analyze(
98
+ parseResult.ast,
99
+ options.variables,
100
+ options.inputType,
101
+ { cursorMode: !!parserOptions?.mode && parserOptions.mode === 'lsp' }
102
+ );
103
+
104
+ return result;
96
105
  }
97
106
 
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
-
109
- // Handle error nodes - process them for diagnostics but don't traverse
110
- if (node.type === 'Error') {
111
- // Diagnostics already added in annotateAST
112
- return;
113
- }
114
-
115
- // If we've already stopped at cursor, don't continue
116
- if (this.stoppedAtCursor) {
117
- return;
107
+ /**
108
+ * Main entry point for context-flow analysis.
109
+ * Analyzes a node with the given context.
110
+ */
111
+ private async analyzeNode(node: ASTNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
112
+ // Handle cursor nodes for completion
113
+ if (isCursorNode(node)) {
114
+ return this.analyzeCursorNode(node as AnyCursorNode, context);
118
115
  }
116
+
117
+ let result: InternalAnalysisResult;
119
118
 
120
119
  switch (node.type) {
121
120
  case NodeType.Binary:
122
- this.visitBinaryOperator(node as BinaryNode);
121
+ result = await this.analyzeBinary(node as BinaryNode, context);
123
122
  break;
124
- case NodeType.Identifier:
125
- this.visitIdentifier(node as IdentifierNode);
123
+ case NodeType.Unary:
124
+ result = await this.analyzeUnary(node as UnaryNode, context);
126
125
  break;
127
126
  case NodeType.Function:
128
- this.visitFunctionCall(node as FunctionNode);
127
+ result = await this.analyzeFunction(node as FunctionNode, context);
128
+ break;
129
+ case NodeType.Variable:
130
+ result = this.analyzeVariable(node as VariableNode, context);
131
+ break;
132
+ case NodeType.Identifier:
133
+ result = await this.analyzeIdentifier(node as IdentifierNode, context);
134
+ break;
135
+ case NodeType.Literal:
136
+ result = this.analyzeLiteral(node as LiteralNode, context);
137
+ break;
138
+ case NodeType.TemporalLiteral:
139
+ result = this.analyzeTemporalLiteral(node as TemporalLiteralNode, context);
129
140
  break;
130
141
  case NodeType.Index:
131
- const indexNode = node as IndexNode;
132
- this.visitNode(indexNode.expression);
133
- this.visitNode(indexNode.index);
142
+ result = await this.analyzeIndex(node as IndexNode, context);
134
143
  break;
135
144
  case NodeType.Collection:
136
- (node as CollectionNode).elements.forEach(el => this.visitNode(el));
137
- break;
138
- case NodeType.Unary:
139
- this.visitNode((node as UnaryNode).operand);
145
+ result = await this.analyzeCollection(node as CollectionNode, context);
140
146
  break;
141
147
  case NodeType.MembershipTest:
142
- this.visitMembershipTest(node as MembershipTestNode);
148
+ result = await this.analyzeMembershipTest(node as MembershipTestNode, context);
143
149
  break;
144
150
  case NodeType.TypeCast:
145
- this.visitTypeCast(node as TypeCastNode);
151
+ result = await this.analyzeTypeCast(node as TypeCastNode, context);
146
152
  break;
147
- case NodeType.Variable:
148
- this.validateVariable((node as VariableNode).name, node);
153
+ case NodeType.Quantity:
154
+ result = this.analyzeQuantity(node as QuantityNode, context);
149
155
  break;
150
- case NodeType.Literal:
151
- case NodeType.TypeOrIdentifier:
152
- case NodeType.TypeReference:
153
- // These are always valid
156
+ case 'Error':
157
+ result = this.analyzeError(node as ErrorNode, context);
154
158
  break;
159
+ default:
160
+ result = {
161
+ type: { type: 'Any', singleton: false },
162
+ diagnostics: [toDiagnostic(Errors.unknownNodeType(String((node as any)?.type), (node as any)?.range))]
163
+ };
155
164
  }
165
+
166
+ // Annotate the node with type information
167
+ node.typeInfo = result.type;
168
+
169
+ return result;
156
170
  }
157
171
 
158
- private visitBinaryOperator(node: BinaryNode): void {
159
- this.visitNode(node.left);
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;
172
+ /**
173
+ * Analyzes binary operators with special handling for union and dot.
174
+ */
175
+ private async analyzeBinary(node: BinaryNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
176
+ const diagnostics: Diagnostic[] = [];
177
+
178
+ // Special handling for union operator - fork context for each branch
179
+ if (node.operator === '|') {
180
+ const leftResult = await this.analyzeNode(node.left, context.fork());
181
+ if (this.stoppedAtCursor) {
182
+ return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
184
183
  }
184
+
185
+ const rightResult = await this.analyzeNode(node.right, context.fork());
186
+
187
+ diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
188
+
189
+ // Preserve left operand type per operator signature (leftType)
190
+ const type = { ...leftResult.type, singleton: false };
191
+
192
+ return {
193
+ type,
194
+ diagnostics,
195
+ context // Return original context unchanged - no variable leakage
196
+ };
185
197
  }
186
-
187
- // Special handling for dot operator with function on right side
188
- if (node.operator === '.' && node.right.type === NodeType.Function) {
189
- const funcNode = node.right as FunctionNode;
190
- if (funcNode.name.type === NodeType.Identifier) {
191
- const funcName = (funcNode.name as IdentifierNode).name;
192
- const func = registry.getFunction(funcName);
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) {
213
- const inputTypeStr = this.typeToString(node.left.typeInfo);
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)
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
- }
239
- }
240
- }
198
+
199
+ // Special handling for dot operator - flow context through
200
+ if (node.operator === '.') {
201
+ const leftResult = await this.analyzeNode(node.left, context);
202
+ if (this.stoppedAtCursor) {
203
+ return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
241
204
  }
205
+
206
+ // Right side gets left's output as input, with any context changes
207
+ const rightContext = (leftResult.context || context).withInputType(leftResult.type);
208
+ const rightResult = await this.analyzeNode(node.right, rightContext);
209
+
210
+ diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
211
+
212
+ return {
213
+ type: rightResult.type,
214
+ diagnostics,
215
+ context: rightResult.context // Propagate context changes (for defineVariable)
216
+ };
242
217
  }
243
-
244
- this.visitNode(node.right);
245
-
246
- // For dot operator, we don't need to check operator types
247
- if (node.operator === '.') {
248
- return;
218
+
219
+ // Handle other binary operators
220
+ const leftResult = await this.analyzeNode(node.left, context);
221
+ if (this.stoppedAtCursor) {
222
+ return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
249
223
  }
250
224
 
251
- const op = registry.getOperatorDefinition(node.operator);
252
- if (!op) {
253
- this.diagnostics.push(
254
- toDiagnostic(Errors.unknownOperator(node.operator, node.range))
255
- );
256
- return;
225
+ // Check if right side is a cursor node - if so, set proper context
226
+ if (this.cursorMode && isCursorNode(node.right)) {
227
+ this.stoppedAtCursor = true;
228
+ this.cursorContext = {
229
+ cursorNode: node.right as AnyCursorNode,
230
+ typeBeforeCursor: leftResult.type,
231
+ expectedType: undefined
232
+ };
233
+ return {
234
+ type: { type: 'Any', singleton: false },
235
+ diagnostics: leftResult.diagnostics
236
+ };
257
237
  }
258
238
 
259
- // Type check if we have type information
260
- if (node.left.typeInfo && node.right.typeInfo) {
261
- this.checkBinaryOperatorTypes(node, op);
239
+ // For most operators, right side evaluates with original context (not left's output)
240
+ const rightResult = await this.analyzeNode(node.right, context);
241
+
242
+ diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
243
+
244
+ // Get operator definition for type checking
245
+ const operatorDef = registry.getOperatorDefinition(node.operator);
246
+ if (!operatorDef) {
247
+ diagnostics.push(toDiagnostic(Errors.unknownOperator(node.operator, node.range)));
248
+ return {
249
+ type: { type: 'Any', singleton: false },
250
+ diagnostics
251
+ };
252
+ }
253
+
254
+ // Check operator signatures for type compatibility
255
+ if (operatorDef.signatures && operatorDef.signatures.length > 0) {
256
+ const matchingSignature = matchOperatorSignature(leftResult.type, rightResult.type, operatorDef) || null;
257
+ if (!matchingSignature) {
258
+ // No matching signature found - report type error
259
+ // But don't report if either side is Any (could be from an error)
260
+ if (leftResult.type.type !== 'Any' && rightResult.type.type !== 'Any') {
261
+ const leftTypeStr = leftResult.type.singleton ? leftResult.type.type : `${leftResult.type.type}[]`;
262
+ const rightTypeStr = rightResult.type.singleton ? rightResult.type.type : `${rightResult.type.type}[]`;
263
+ diagnostics.push(this.createError(
264
+ node,
265
+ `Operator '${node.operator}' cannot be applied to types ${leftTypeStr} and ${rightTypeStr}`,
266
+ ErrorCodes.OPERATOR_TYPE_MISMATCH
267
+ ));
268
+ }
269
+ return {
270
+ type: { type: 'Any', singleton: false },
271
+ diagnostics
272
+ };
273
+ }
274
+
275
+ // Determine result type from matching signature
276
+ const resultType = resolveResultType(matchingSignature.result as any, {
277
+ input: context.inputType,
278
+ left: leftResult.type,
279
+ right: rightResult.type,
280
+ });
281
+
282
+ return {
283
+ type: resultType,
284
+ diagnostics
285
+ };
262
286
  }
287
+
288
+ // If no signatures defined, return Any type
289
+ return {
290
+ type: { type: 'Any', singleton: false },
291
+ diagnostics
292
+ };
263
293
  }
264
294
 
265
- private visitIdentifier(node: IdentifierNode): void {
266
- this.validateVariable(node.name, node);
295
+ /**
296
+ * Analyzes function calls, delegating to function's analyze method if available.
297
+ */
298
+ private async analyzeFunction(node: FunctionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
299
+ const diagnostics: Diagnostic[] = [];
300
+
301
+ const functionName = this.getFunctionName(node);
302
+ if (!functionName) {
303
+ diagnostics.push(this.createError(node.name, 'Invalid function name', ErrorCodes.INVALID_SYNTAX));
304
+ return { type: { type: 'Any', singleton: false }, diagnostics };
305
+ }
306
+
307
+ const funcDef = registry.getFunction(functionName);
308
+ if (!funcDef) {
309
+ diagnostics.push(toDiagnostic(Errors.unknownFunction(functionName, node.range)));
310
+ return { type: { type: 'Any', singleton: false }, diagnostics };
311
+ }
312
+
313
+ const arity = this.validateArity(funcDef, node, functionName);
314
+ diagnostics.push(...arity.diagnostics);
315
+
316
+ // Early union rules for ofType/is/as
317
+ diagnostics.push(...this.validateUnionTypeFilters(functionName, node, context));
318
+
319
+ // Custom analyze
320
+ if (funcDef.analyze) {
321
+ const result = funcDef.analyze(context, node.arguments);
322
+ const analysisResult = result instanceof Promise ? await result : result;
323
+ return {
324
+ ...analysisResult,
325
+ diagnostics: [...diagnostics, ...analysisResult.diagnostics]
326
+ };
327
+ }
328
+
329
+ // Default path: analyze args
330
+ const argAnalysis = await this.analyzeArguments(funcDef, node, context, functionName);
331
+ diagnostics.push(...argAnalysis.diagnostics);
332
+ if (this.stoppedAtCursor) {
333
+ return { type: { type: 'Any', singleton: false }, diagnostics };
334
+ }
335
+
336
+ // Signature matching and diagnostics
337
+ const signatureResult = this.matchAndDiagnoseSignature(
338
+ funcDef,
339
+ context.inputType,
340
+ argAnalysis.argTypes,
341
+ node,
342
+ functionName,
343
+ arity.hasError
344
+ );
345
+ diagnostics.push(...signatureResult.diagnostics);
346
+ if (signatureResult.earlyReturn) {
347
+ return { type: signatureResult.earlyReturn, diagnostics };
348
+ }
349
+
350
+ // Empty propagation
351
+ if (this.propagatesEmpty(funcDef, context.inputType, argAnalysis.argTypes)) {
352
+ return {
353
+ type: { type: 'Any', singleton: false, isEmpty: true },
354
+ diagnostics,
355
+ context
356
+ };
357
+ }
358
+
359
+ // Result inference
360
+ let resultType = await this.inferFunctionResultType(
361
+ funcDef,
362
+ node,
363
+ context,
364
+ argAnalysis.argTypes,
365
+ signatureResult.match
366
+ );
367
+
368
+ if (functionName === 'where') {
369
+ resultType = { ...resultType, singleton: false };
370
+ }
371
+
372
+ return { type: resultType, diagnostics, context };
267
373
  }
268
374
 
269
- private visitFunctionCall(node: FunctionNode): void {
375
+ private getFunctionName(node: FunctionNode): string | null {
270
376
  if (node.name.type === NodeType.Identifier) {
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))
377
+ return (node.name as IdentifierNode).name;
378
+ }
379
+ return null;
380
+ }
381
+
382
+ private validateArity(
383
+ funcDef: FunctionDefinition,
384
+ node: FunctionNode,
385
+ functionName: string
386
+ ): { diagnostics: Diagnostic[]; hasError: boolean } {
387
+ const diagnostics: Diagnostic[] = [];
388
+ let hasError = false;
389
+ if (funcDef.signatures && funcDef.signatures.length > 0) {
390
+ const signature = funcDef.signatures[0];
391
+ if (signature) {
392
+ const params = signature.parameters || [];
393
+ const requiredCount = params.filter(p => !p.optional).length;
394
+ const maxCount = params.length;
395
+ const actualCount = node.arguments.length;
396
+
397
+ if (actualCount < requiredCount) {
398
+ diagnostics.push(
399
+ this.createError(
400
+ node,
401
+ `${functionName} expects at least ${requiredCount} argument${requiredCount !== 1 ? 's' : ''}, got ${actualCount}`,
402
+ ErrorCodes.WRONG_ARGUMENT_COUNT
403
+ )
291
404
  );
405
+ hasError = true;
406
+ } else if (actualCount > maxCount) {
407
+ diagnostics.push(
408
+ this.createError(
409
+ node,
410
+ `${functionName} expects at most ${maxCount} argument${maxCount !== 1 ? 's' : ''}, got ${actualCount}`,
411
+ ErrorCodes.WRONG_ARGUMENT_COUNT
412
+ )
413
+ );
414
+ hasError = true;
292
415
  }
293
416
  }
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;
417
+ }
418
+ return { diagnostics, hasError };
419
+ }
420
+
421
+ private validateUnionTypeFilters(
422
+ functionName: string,
423
+ node: FunctionNode,
424
+ context: AnalysisContext
425
+ ): Diagnostic[] {
426
+ const diagnostics: Diagnostic[] = [];
427
+ if (!['ofType', 'is', 'as'].includes(functionName) || node.arguments.length === 0) {
428
+ return diagnostics;
429
+ }
430
+ const inputType = context.inputType;
431
+ if (!isUnionType(inputType)) {
432
+ return diagnostics;
433
+ }
434
+ const typeArg = node.arguments[0]!;
435
+ let targetType: string | undefined;
436
+ if (typeArg.type === NodeType.Identifier) {
437
+ targetType = (typeArg as IdentifierNode).name;
438
+ }
439
+ if (!targetType) {
440
+ return diagnostics;
441
+ }
442
+ const diag = validateUnionChoice(inputType, targetType, typeArg.range || node.range, 'invalid-type-filter', 'Type');
443
+ if (diag && functionName === 'ofType') diagnostics.push(diag);
444
+ return diagnostics;
445
+ }
446
+
447
+ private async analyzeArguments(
448
+ funcDef: FunctionDefinition,
449
+ node: FunctionNode,
450
+ context: AnalysisContext,
451
+ functionName: string
452
+ ): Promise<{ argTypes: TypeInfo[]; diagnostics: Diagnostic[] }> {
453
+ const diagnostics: Diagnostic[] = [];
454
+ const argTypes: TypeInfo[] = [];
455
+ const signature = funcDef.signatures?.[0];
456
+ const params = signature?.parameters || [];
457
+
458
+ for (let i = 0; i < node.arguments.length; i++) {
459
+ const arg = node.arguments[i]!;
460
+ const param = params[i];
461
+ const isTypeParameter = !!param?.typeReference;
462
+
463
+ if (isTypeParameter) {
464
+ argTypes.push({ type: 'TypeReference' as TypeName, singleton: true });
465
+ continue;
466
+ }
467
+
468
+ if (param?.expression) {
469
+ const itemType = { ...context.inputType, singleton: true };
470
+ const exprContext = context
471
+ .withSystemVariable('$this', itemType)
472
+ .withSystemVariable('$index', { type: 'Integer', singleton: true });
473
+ const argResult = await this.analyzeNode(arg, exprContext);
474
+ diagnostics.push(...argResult.diagnostics);
475
+ argTypes.push(argResult.type);
476
+ if (this.stoppedAtCursor) {
477
+ break;
478
+ }
479
+ continue;
480
+ }
481
+
482
+ const thisType = context.systemVariables.get('$this') || context.inputType;
483
+ const argContext = context.withInputType(thisType);
484
+ const argResult = await this.analyzeNode(arg, argContext);
485
+ diagnostics.push(...argResult.diagnostics);
486
+ argTypes.push(argResult.type);
487
+ if (this.stoppedAtCursor) {
488
+ break;
489
+ }
490
+ }
491
+
492
+ return { argTypes, diagnostics };
493
+ }
494
+
495
+ private matchAndDiagnoseSignature(
496
+ funcDef: FunctionDefinition,
497
+ actualInput: TypeInfo,
498
+ argTypes: TypeInfo[],
499
+ node: FunctionNode,
500
+ functionName: string,
501
+ hasArityError: boolean
502
+ ): { match: FunctionSignature | null; diagnostics: Diagnostic[]; earlyReturn?: TypeInfo } {
503
+ const diagnostics: Diagnostic[] = [];
504
+ let match: FunctionSignature | null = null;
505
+
506
+ if (!hasArityError && funcDef.signatures && funcDef.signatures.length > 0) {
507
+ match = matchFunctionSignature(actualInput, argTypes, funcDef) || null;
508
+
509
+ if (!match) {
510
+ const inputIsEmpty = isEmptyCollection(actualInput);
511
+ if (inputIsEmpty && !funcDef.doesNotPropagateEmpty) {
512
+ const sig = funcDef.signatures[0];
513
+ if (sig) {
514
+ diagnostics.push(
515
+ ...checkParamTypes(sig, argTypes, node.arguments, {
516
+ warnOnSingletonOnly: false,
517
+ doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
518
+ treatEmptyAsWarning: true,
519
+ errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
520
+ })
521
+ );
522
+ }
523
+ } else {
524
+ let inputMatchingSignature: FunctionSignature | null = null;
525
+ for (const sig of funcDef.signatures) {
526
+ let inputMatches = true;
527
+ if (sig.input) {
528
+ const expectedInput = sig.input;
529
+ const singletonMatch = !expectedInput.singleton || actualInput.singleton === true;
530
+ const typeMatch =
531
+ expectedInput.type === 'Any' ||
532
+ actualInput.type === 'Any' ||
533
+ expectedInput.type === actualInput.type ||
534
+ (expectedInput.type === 'Decimal' && actualInput.type === 'Integer');
535
+ inputMatches = singletonMatch && typeMatch;
536
+ }
537
+ if (inputMatches) {
538
+ inputMatchingSignature = sig;
539
+ break;
540
+ }
312
541
  }
313
-
314
- if (targetType) {
315
- const validChoice = inputType.modelContext.choices.find((choice: any) =>
316
- choice.type === targetType || choice.code === targetType
542
+
543
+ if (inputMatchingSignature && inputMatchingSignature.parameters) {
544
+ diagnostics.push(
545
+ ...checkParamTypes(inputMatchingSignature, argTypes, node.arguments, {
546
+ warnOnSingletonOnly: true,
547
+ doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
548
+ errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
549
+ })
317
550
  );
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
- });
551
+ } else {
552
+ const actualTypeStr = actualInput.singleton ? actualInput.type : `${actualInput.type}[]`;
553
+ const hasSingletonSignature = funcDef.signatures.some(sig => sig.input?.singleton && sig.input.type === actualInput.type);
554
+ const permissive = ['anyFalse', 'anyTrue'];
555
+ if (hasSingletonSignature && !actualInput.singleton) {
556
+ diagnostics.push(
557
+ this.createError(
558
+ node,
559
+ `${functionName} expects a singleton value, but received collection type ${actualTypeStr}`,
560
+ ErrorCodes.SINGLETON_REQUIRED
561
+ )
562
+ );
563
+ } else if (!permissive.includes(functionName)) {
564
+ const expectedTypes = funcDef.signatures
565
+ .map(sig => (sig.input ? (sig.input.singleton ? sig.input.type : `${sig.input.type}[]`) : 'Any'))
566
+ .filter((v, i, a) => a.indexOf(v) === i)
567
+ .join(' or ');
568
+ diagnostics.push(
569
+ this.createError(
570
+ node,
571
+ `Cannot apply ${functionName}() to ${actualTypeStr}. Function expects ${expectedTypes}.`,
572
+ ErrorCodes.INVALID_OPERAND_TYPE
573
+ )
574
+ );
328
575
  }
329
576
  }
330
577
  }
331
- }
332
-
333
- const func = registry.getFunction(funcName);
334
-
335
- if (!func) {
336
- this.diagnostics.push(
337
- toDiagnostic(Errors.unknownFunction(funcName, node.range))
338
- );
339
578
  } else {
340
- // Check argument count based on signature
341
- const params = func.signatures?.[0]?.parameters || [];
342
- const requiredParams = params.filter(p => !p.optional).length;
343
- const maxParams = params.length;
344
-
345
- if (node.arguments.length < requiredParams) {
346
- this.diagnostics.push(
347
- toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
579
+ if (match.parameters) {
580
+ diagnostics.push(
581
+ ...checkParamTypes(match, argTypes, node.arguments, {
582
+ warnOnSingletonOnly: true,
583
+ doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
584
+ errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
585
+ })
348
586
  );
349
- } else if (node.arguments.length > maxParams) {
350
- this.diagnostics.push(
351
- toDiagnostic(Errors.wrongArgumentCount(funcName, maxParams, node.arguments.length, node.range))
352
- );
353
- }
354
-
355
- // Type check arguments if we have type information
356
- if (node.typeInfo || node.arguments.some(arg => arg.typeInfo)) {
357
- this.checkFunctionArgumentTypes(node, func);
587
+ } else {
588
+ const permissive = ['anyFalse', 'anyTrue'];
589
+ if (permissive.includes(functionName)) {
590
+ return { match, diagnostics, earlyReturn: { type: 'Boolean', singleton: true } };
591
+ }
358
592
  }
359
593
  }
360
594
  }
361
-
362
- node.arguments.forEach(arg => this.visitNode(arg));
595
+
596
+ return { match, diagnostics };
363
597
  }
364
598
 
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);
599
+ private propagatesEmpty(
600
+ funcDef: FunctionDefinition,
601
+ inputType: TypeInfo,
602
+ argTypes: TypeInfo[]
603
+ ): boolean {
604
+ if (funcDef.doesNotPropagateEmpty) {
605
+ return false;
606
+ }
607
+ const inputIsEmpty = isEmptyCollection(inputType);
608
+ const hasEmptyArgument = argTypes.some(argType => isEmptyCollection(argType));
609
+ return inputIsEmpty || hasEmptyArgument;
404
610
  }
405
611
 
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
- }
612
+ private async inferFunctionResultType(
613
+ funcDef: FunctionDefinition,
614
+ node: FunctionNode,
615
+ context: AnalysisContext,
616
+ argTypes: TypeInfo[],
617
+ matchingSignature: FunctionSignature | null
618
+ ): Promise<TypeInfo> {
619
+ if (funcDef.inferResultType) {
620
+ return funcDef.inferResultType(this, node, context.inputType);
621
+ }
622
+ if (matchingSignature) {
623
+ return resolveResultType(matchingSignature.result as any, {
624
+ input: context.inputType,
625
+ firstParam: argTypes[0],
626
+ });
442
627
  }
443
-
444
- this.visitNode(node.expression);
628
+ return context.inputType;
445
629
  }
446
630
 
447
- // Unified variable validation to eliminate duplication
448
- private validateVariable(name: string, node: ASTNode): void {
449
- if (name.startsWith('$')) {
450
- if (!this.variables.has(name)) {
451
- this.diagnostics.push(
452
- toDiagnostic(Errors.unknownVariable(name, node.range))
453
- );
454
- }
455
- } else if (name.startsWith('%')) {
456
- const varName = name.substring(1);
457
- if (!this.variables.has(varName)) {
458
- this.diagnostics.push(
459
- toDiagnostic(Errors.unknownUserVariable(name, node.range))
460
- );
631
+ /**
632
+ * Analyzes variable references, checking against context.
633
+ */
634
+ private analyzeVariable(node: VariableNode, context: AnalysisContext): InternalAnalysisResult {
635
+ const varName = node.name;
636
+ const diagnostics: Diagnostic[] = [];
637
+
638
+ // Check if it's a user variable (starts with %)
639
+ if (varName.startsWith('%')) {
640
+ // Special handling for %context - it's a built-in environment variable
641
+ // that always returns the original input to the evaluation engine
642
+ if (varName === '%context') {
643
+ // %context returns the root input type (the original input to evaluate())
644
+ // In the analyzer, we track this as the initial input type
645
+ return { type: context.inputType, diagnostics, context };
461
646
  }
462
- }
463
- }
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);
647
+
648
+ const name = varName.substring(1); // Remove % prefix
649
+ const varType = context.userVariables.get(name);
650
+
651
+ if (!varType) {
652
+ // If we have dynamic variables in scope, we can't be sure this is an error
653
+ if (context.hasDynamicVariables) {
654
+ diagnostics.push(this.createWarning(node, `Variable '${varName}' may not be defined (dynamic variables in scope)`));
655
+ // Return Any type since we don't know the actual type
656
+ return { type: { type: 'Any', singleton: false }, diagnostics };
657
+ } else {
658
+ // No dynamic variables, so this is definitely an error
659
+ diagnostics.push(this.createError(node, Errors.unknownUserVariable(varName).message, ErrorCodes.UNKNOWN_USER_VARIABLE));
660
+ return { type: { type: 'Any', singleton: false }, diagnostics };
477
661
  }
478
662
  }
663
+
664
+ // Attach type info to the node for backward compatibility
665
+ node.typeInfo = varType;
666
+ return { type: varType, diagnostics, context };
479
667
  }
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
- }
668
+
669
+ // Check system variables
670
+ const sysVarType = context.systemVariables.get(varName);
671
+ if (sysVarType) {
672
+ // Attach type info to the node for backward compatibility
673
+ node.typeInfo = sysVarType;
674
+ return { type: sysVarType, diagnostics, context };
502
675
  }
503
-
504
- return vars;
676
+
677
+ // Unknown variable
678
+ diagnostics.push(this.createError(node, `Unknown variable: ${varName}`, ErrorCodes.UNKNOWN_VARIABLE));
679
+ return { type: { type: 'Any', singleton: false }, diagnostics };
505
680
  }
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
- }
681
+
682
+ /**
683
+ * Analyzes identifier nodes (property access).
684
+ */
685
+ private async analyzeIdentifier(node: IdentifierNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
686
+ const name = 'name' in node ? node.name : '';
687
+ const diagnostics: Diagnostic[] = [];
688
+
689
+ // Try to use model provider for accurate type information
690
+ if (context.modelProvider) {
691
+ // First try to navigate from input type (property access)
692
+ const elementType = await context.modelProvider.getElementType(context.inputType, name);
693
+ if (elementType) {
694
+ return {
695
+ type: elementType,
696
+ diagnostics,
697
+ context
698
+ };
533
699
  }
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
- }
700
+ // Chain-head rule: at the head of a navigation chain, allow treating the
701
+ // identifier as a known type to seed the chain (e.g., Patient.name)
702
+ if ((context as any)._chainHead === true) {
703
+ const typeInfo = await context.modelProvider.getType(name);
704
+ if (typeInfo) {
705
+ return {
706
+ type: typeInfo,
707
+ diagnostics,
708
+ context
709
+ };
566
710
  }
567
711
  }
712
+
713
+ // If property not found and we have a concrete non-union type, report warning
714
+ // FHIRPath returns empty for unknown properties, not an error
715
+ const mc: any = context.inputType.modelContext;
716
+ const isUnion = !!(mc && typeof mc === 'object' && 'isUnion' in mc && mc.isUnion);
717
+ if (context.inputType.namespace && context.inputType.name && context.inputType.modelContext && !isUnion) {
718
+ const typeStr = `${context.inputType.namespace}.${context.inputType.name}`;
719
+ diagnostics.push(toDiagnostic(Errors.unknownProperty(name, typeStr, node.range), DiagnosticSeverity.Warning));
720
+ return {
721
+ type: { type: 'Any', singleton: false },
722
+ diagnostics,
723
+ context
724
+ };
725
+ }
568
726
  }
569
727
 
570
- return varsWithTypes;
728
+ // Without a model provider, we can't know the type
729
+ // Return Any type - don't make assumptions
730
+ return {
731
+ type: { type: 'Any', singleton: false },
732
+ diagnostics,
733
+ context
734
+ };
571
735
  }
572
736
 
573
-
574
- // Type inference methods
575
- private async inferType(node: ASTNode, inputType?: TypeInfo): Promise<TypeInfo> {
576
- // Handle error nodes
577
- if (node.type === 'Error') {
578
- return this.inferErrorNodeType(node as ErrorNode, inputType);
579
- }
737
+ /**
738
+ * Analyzes literal values.
739
+ */
740
+ private analyzeLiteral(node: LiteralNode, context: AnalysisContext): InternalAnalysisResult {
741
+ let type: TypeInfo;
580
742
 
581
- switch (node.type) {
582
- case NodeType.Literal:
583
- return this.inferLiteralType(node as LiteralNode);
584
-
585
- case NodeType.Binary:
586
- return await this.inferBinaryType(node as BinaryNode, inputType);
587
-
588
- case NodeType.Unary:
589
- return this.inferUnaryType(node as UnaryNode);
590
-
591
- case NodeType.Function:
592
- return await this.inferFunctionType(node as FunctionNode, inputType);
593
-
594
- case NodeType.Identifier:
595
- return await this.inferIdentifierType(node as IdentifierNode, inputType);
596
-
597
- case NodeType.Variable:
598
- return this.inferVariableType(node as VariableNode);
599
-
600
- case NodeType.Collection:
601
- return await this.inferCollectionType(node as CollectionNode);
602
-
603
- case NodeType.TypeCast:
604
- return await this.inferTypeCastType(node as TypeCastNode);
605
-
606
- case NodeType.MembershipTest:
607
- return { type: 'Boolean', singleton: true };
608
-
609
- case NodeType.TypeOrIdentifier:
610
- return await this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
611
-
612
- default:
613
- return { type: 'Any', singleton: false };
614
- }
615
- }
616
-
617
- private inferErrorNodeType(errorNode: ErrorNode, inputType?: TypeInfo): TypeInfo {
618
- // For error nodes, return a generic type that allows partial analysis to continue
619
- // This enables type checking for valid parts of broken expressions
620
- return { type: 'Any', singleton: false };
621
- }
622
-
623
- private inferLiteralType(node: LiteralNode): TypeInfo {
624
743
  switch (node.valueType) {
625
744
  case 'string':
626
- return { type: 'String', singleton: true };
745
+ type = { type: 'String', singleton: true };
746
+ break;
627
747
  case 'number':
628
- const num = node.value as number;
629
- return {
630
- type: Number.isInteger(num) ? 'Integer' : 'Decimal',
631
- singleton: true
632
- };
748
+ // Check if integer or decimal
749
+ type = Number.isInteger(node.value)
750
+ ? { type: 'Integer', singleton: true }
751
+ : { type: 'Decimal', singleton: true };
752
+ break;
633
753
  case 'boolean':
634
- return { type: 'Boolean', singleton: true };
754
+ type = { type: 'Boolean', singleton: true };
755
+ break;
635
756
  case 'date':
636
- return { type: 'Date', singleton: true };
637
- case 'datetime':
638
- return { type: 'DateTime', singleton: true };
757
+ type = { type: 'Date', singleton: true };
758
+ break;
639
759
  case 'time':
640
- return { type: 'Time', singleton: true };
641
- case 'null':
642
- return { type: 'Any', singleton: false }; // Empty collection
760
+ type = { type: 'Time', singleton: true };
761
+ break;
762
+ case 'datetime':
763
+ type = { type: 'DateTime', singleton: true };
764
+ break;
643
765
  default:
644
- return { type: 'Any', singleton: true };
766
+ type = { type: 'Any', singleton: true };
645
767
  }
646
- }
647
-
648
- private async inferBinaryType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
649
- const operator = registry.getOperatorDefinition(node.operator);
650
- if (!operator) {
651
- return { type: 'Any', singleton: false };
652
- }
653
-
654
- // For navigation (dot operator), we need special handling
655
- if (node.operator === '.') {
656
- return await this.inferNavigationType(node, inputType);
657
- }
658
-
659
- // Infer types of operands
660
- const leftType = await this.inferType(node.left, inputType);
661
- const rightType = await this.inferType(node.right, inputType);
662
768
 
663
- // Find matching signature
664
- for (const sig of operator.signatures) {
665
- if (this.isTypeCompatible(leftType, sig.left) &&
666
- this.isTypeCompatible(rightType, sig.right)) {
667
- return this.resolveResultType(sig.result, inputType, leftType, rightType);
668
- }
669
- }
670
-
671
- // Default to first signature's result type
672
- const defaultResult = operator.signatures[0]?.result || { type: 'Any', singleton: false };
673
- return this.resolveResultType(defaultResult, inputType, leftType, rightType);
769
+ return { type, diagnostics: [] };
674
770
  }
675
771
 
676
- private async inferNavigationType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
677
- const leftType = await this.inferType(node.left, inputType);
678
-
679
- // If the right side is a function, return the function's type
680
- if (node.right.type === NodeType.Function) {
681
- return await this.inferType(node.right, leftType);
682
- }
772
+ private analyzeTemporalLiteral(node: TemporalLiteralNode, context: AnalysisContext): InternalAnalysisResult {
773
+ let type: TypeInfo;
683
774
 
684
- // If we have a model provider and the right side is an identifier
685
- if (this.modelProvider && node.right.type === NodeType.Identifier) {
686
- const propertyName = (node.right as IdentifierNode).name;
687
-
688
- // Use getElementType to navigate the property
689
- const resultType = await this.modelProvider.getElementType(leftType, propertyName);
690
- if (resultType) {
691
- return resultType;
692
- }
693
-
694
- // If property not found and we have a concrete type from model provider, report error
695
- // Skip diagnostics for union types - they may have dynamic properties
696
- if (leftType.namespace && leftType.name && leftType.modelContext &&
697
- !(leftType.modelContext as any).isUnion) {
698
- this.diagnostics.push(
699
- toDiagnostic(Errors.unknownProperty(propertyName, `${leftType.namespace}.${leftType.name}`, node.right.range))
700
- );
701
- }
775
+ switch (node.valueType) {
776
+ case 'date':
777
+ type = { type: 'Date', singleton: true };
778
+ break;
779
+ case 'time':
780
+ type = { type: 'Time', singleton: true };
781
+ break;
782
+ case 'datetime':
783
+ type = { type: 'DateTime', singleton: true };
784
+ break;
785
+ default:
786
+ type = { type: 'Any', singleton: true };
702
787
  }
703
788
 
704
- // Default navigation behavior
705
- return { type: 'Any', singleton: false };
789
+ return { type, diagnostics: [] };
706
790
  }
707
-
708
- private inferUnaryType(node: UnaryNode): TypeInfo {
709
- const operator = registry.getOperatorDefinition(node.operator);
710
- if (!operator) {
711
- return { type: 'Any', singleton: false };
712
- }
713
-
714
- // Unary operators typically have one signature
715
- const signature = operator.signatures[0];
716
- if (signature && typeof signature.result === 'object') {
717
- return signature.result;
791
+
792
+ /**
793
+ * Analyzes unary operators.
794
+ */
795
+ private async analyzeUnary(node: UnaryNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
796
+ const operandResult = await this.analyzeNode(node.operand, context);
797
+ const diagnostics: Diagnostic[] = [...operandResult.diagnostics];
798
+ const opDef = registry.getOperatorDefinition(node.operator);
799
+
800
+ if (opDef && opDef.signatures && opDef.signatures.length > 0) {
801
+ // Use first signature's result if defined
802
+ const sig = opDef.signatures[0]!;
803
+ const resultType = typeof sig.result === 'object' ? sig.result : operandResult.type;
804
+ return { type: resultType, diagnostics };
718
805
  }
719
-
720
- return { type: 'Any', singleton: false };
806
+
807
+ // Fallback: preserve operand type
808
+ return { type: operandResult.type, diagnostics };
721
809
  }
722
-
723
- private async inferFunctionType(node: FunctionNode, inputType?: TypeInfo): Promise<TypeInfo> {
724
- if (node.name.type !== NodeType.Identifier) {
725
- return { type: 'Any', singleton: false };
726
- }
727
-
728
- const funcName = (node.name as IdentifierNode).name;
729
- const func = registry.getFunction(funcName);
730
-
731
- if (!func) {
732
- return { type: 'Any', singleton: false };
733
- }
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 };
810
+
811
+ /**
812
+ * Analyzes index operations.
813
+ */
814
+ private async analyzeIndex(node: IndexNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
815
+ const exprResult = await this.analyzeNode(node.expression, context);
816
+ const indexResult = await this.analyzeNode(node.index, context);
817
+ const diagnostics: Diagnostic[] = [...exprResult.diagnostics, ...indexResult.diagnostics];
818
+
819
+ // Index should be Integer singleton; if unknown, skip strict error
820
+ const idxType = indexResult.type;
821
+ const isIntegerSingleton = idxType.type === 'Integer' && idxType.singleton === true;
822
+ if (!isIntegerSingleton && idxType.type !== 'Any') {
823
+ diagnostics.push(this.createError(node.index, 'Index must be an Integer singleton', ErrorCodes.ARGUMENT_TYPE_MISMATCH));
771
824
  }
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 };
825
+
826
+ // Result is the element type (singleton) of the expression collection
827
+ const exprType = exprResult.type;
828
+ const resultType = { ...exprType, singleton: true };
829
+ return { type: resultType, diagnostics };
830
+ }
831
+
832
+ /**
833
+ * Analyzes quantity literals.
834
+ */
835
+ private analyzeQuantity(node: QuantityNode, context: AnalysisContext): InternalAnalysisResult {
836
+ return {
837
+ type: { type: 'Quantity', singleton: true },
838
+ diagnostics: [],
839
+ context
840
+ };
841
+ }
842
+
843
+ /**
844
+ * Analyzes collection literals.
845
+ */
846
+ private async analyzeCollection(node: CollectionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
847
+ const diagnostics: Diagnostic[] = [];
848
+ const isEmpty = node.elements.length === 0;
849
+ const elementTypes: TypeInfo[] = [];
850
+
851
+ for (const element of node.elements) {
852
+ const elemResult = await this.analyzeNode(element, context);
853
+ diagnostics.push(...elemResult.diagnostics);
854
+ elementTypes.push(elemResult.type);
855
+ if (this.stoppedAtCursor) {
856
+ return { type: { type: 'Any', singleton: false }, diagnostics };
787
857
  }
788
- // No arguments at all
789
- return { type: 'Any', singleton: false };
790
858
  }
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;
859
+
860
+ // Infer common element type
861
+ let elementType: TypeInfo = { type: 'Any', singleton: true };
862
+ if (!isEmpty) {
863
+ elementType = elementTypes[0]!;
864
+ for (let i = 1; i < elementTypes.length; i++) {
865
+ const t = elementTypes[i]!;
866
+ if (elementType.type === t.type) {
867
+ // keep; if any is non-singleton, result stays collection anyway
868
+ continue;
869
+ }
870
+ // Promote Integer to Decimal when mixed
871
+ if ((elementType.type === 'Integer' && t.type === 'Decimal') || (elementType.type === 'Decimal' && t.type === 'Integer')) {
872
+ elementType = { type: 'Decimal', singleton: true };
873
+ continue;
798
874
  }
875
+ // Unknown/heterogeneous → Any
876
+ elementType = { type: 'Any', singleton: true };
877
+ break;
799
878
  }
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
-
810
- // Special handling for functions with dynamic result types
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
879
  }
819
-
820
- if (matchingSignature.result === 'inputType') {
821
- // Functions like where() return the same type as input but always as collection
822
- return inputType ? { ...inputType, singleton: false } : { type: 'Any', singleton: false };
823
- } else if (matchingSignature.result === 'inputTypeSingleton') {
824
- // Functions like first(), last() return the same type as input but as singleton
825
- return inputType ? { ...inputType, singleton: true } : { type: 'Any', singleton: true };
826
- } else if (matchingSignature.result === 'parameterType' && node.arguments.length > 0) {
827
- // Functions like select() return the type of the first parameter expression as collection
828
- const paramType = await this.inferType(node.arguments[0]!, inputType);
829
- return { ...paramType, singleton: false };
830
- } else if (typeof matchingSignature.result === 'object') {
831
- return matchingSignature.result;
832
- }
833
-
834
- return { type: 'Any', singleton: false };
880
+
881
+ return { type: { ...elementType, singleton: false, isEmpty }, diagnostics };
835
882
  }
836
-
837
- private async inferIdentifierType(node: IdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
838
- // First, try to navigate from input type (most common case)
839
- if (inputType && this.modelProvider) {
840
- const elementType = await this.modelProvider.getElementType(inputType, node.name);
841
- if (elementType) {
842
- return elementType;
843
- }
844
- }
845
-
846
- // Only check if it's a type name if it starts with uppercase (FHIR convention)
847
- // or if there's no input type context
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);
853
- if (typeInfo) {
854
- return typeInfo;
883
+
884
+ /**
885
+ * Analyzes membership test (is operator).
886
+ */
887
+ private async analyzeMembershipTest(node: MembershipTestNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
888
+ const exprResult = await this.analyzeNode(node.expression, context);
889
+ const diagnostics = [...exprResult.diagnostics];
890
+
891
+ // ModelProvider requirement for non-primitive target types
892
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
893
+ if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
894
+ diagnostics.push(toDiagnostic(Errors.modelProviderRequired('is', node.range)));
895
+ }
896
+
897
+ // Check if testing against a union type
898
+ if (isUnionType(exprResult.type)) {
899
+ const targetType = node.targetType;
900
+ const choices = getUnionChoices(exprResult.type);
901
+ if (choices.length > 0 && !choices.includes(targetType)) {
902
+ diagnostics.push({
903
+ severity: DiagnosticSeverity.Warning,
904
+ code: 'invalid-type-test',
905
+ message: `Type test 'is ${targetType}' will always be false - type not present in union. Available types: ${choices.join(', ')}`,
906
+ range: node.range
907
+ });
855
908
  }
856
909
  }
857
910
 
858
- return { type: 'Any', singleton: false };
911
+ return {
912
+ type: { type: 'Boolean', singleton: true },
913
+ diagnostics
914
+ };
859
915
  }
860
-
861
- private async inferTypeOrIdentifierType(node: TypeOrIdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
862
- // TypeOrIdentifier can be either a type name or a property navigation
863
-
864
- // First, try navigation from input type (most common case)
865
- if (inputType && this.modelProvider) {
866
- const elementType = await this.modelProvider.getElementType(inputType, node.name);
867
- if (elementType) {
868
- return elementType;
916
+
917
+ /**
918
+ * Analyzes type cast (as operator).
919
+ */
920
+ private async analyzeTypeCast(node: TypeCastNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
921
+ const exprResult = await this.analyzeNode(node.expression, context);
922
+ const diagnostics = [...exprResult.diagnostics];
923
+
924
+ // ModelProvider requirement for non-primitive target types
925
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
926
+ if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
927
+ diagnostics.push(toDiagnostic(Errors.modelProviderRequired('as', node.range)));
928
+ }
929
+
930
+ // Check if casting from a union type
931
+ if (isUnionType(exprResult.type)) {
932
+ const targetTypeName = node.targetType;
933
+ const choices = getUnionChoices(exprResult.type);
934
+ if (choices.length > 0 && !choices.includes(targetTypeName)) {
935
+ diagnostics.push({
936
+ severity: DiagnosticSeverity.Warning,
937
+ code: 'invalid-type-cast',
938
+ message: `Type cast 'as ${targetTypeName}' may fail - type not present in union. Available types: ${choices.join(', ')}`,
939
+ range: node.range
940
+ });
869
941
  }
870
942
  }
871
943
 
872
- // Then check if it's a type name (only for uppercase names or no input context)
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);
878
- if (typeInfo) {
879
- return typeInfo;
880
- }
881
- }
944
+ // Type cast changes the type
945
+ const targetType: TypeInfo = {
946
+ type: node.targetType as TypeName,
947
+ singleton: exprResult.type.singleton
948
+ };
882
949
 
883
- return { type: 'Any', singleton: false };
950
+ return {
951
+ type: targetType,
952
+ diagnostics
953
+ };
884
954
  }
885
-
886
- private inferVariableType(node: VariableNode): TypeInfo {
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
909
- }
910
-
911
- // User-defined variables - check with or without % prefix
912
- let varName = node.name;
913
- if (varName.startsWith('%')) {
914
- varName = varName.substring(1);
915
- }
916
-
917
- const userType = this.userVariableTypes.get(varName);
918
- if (userType) {
919
- return userType;
920
- }
921
-
922
- return { type: 'Any', singleton: true };
955
+
956
+ /**
957
+ * Analyzes error nodes.
958
+ */
959
+ private analyzeError(node: ErrorNode, context: AnalysisContext): InternalAnalysisResult {
960
+ return {
961
+ type: { type: 'Any', singleton: false },
962
+ diagnostics: [this.createError(node, node.message, ErrorCodes.INVALID_SYNTAX)]
963
+ };
923
964
  }
924
-
925
- private async inferCollectionType(node: CollectionNode): Promise<TypeInfo> {
926
- if (node.elements.length === 0) {
927
- return { type: 'Any', singleton: false };
928
- }
929
-
930
- // Infer types of all elements
931
- const elementTypes = await Promise.all(node.elements.map(el => this.inferType(el)));
932
-
933
- // If all elements have the same type, use that
934
- const firstType = elementTypes[0];
935
- if (firstType) {
936
- const allSameType = elementTypes.every(t =>
937
- t.type === firstType.type &&
938
- t.namespace === firstType.namespace &&
939
- t.name === firstType.name
940
- );
965
+
966
+ /**
967
+ * Analyzes cursor nodes for completion.
968
+ */
969
+ private analyzeCursorNode(node: AnyCursorNode, context: AnalysisContext): InternalAnalysisResult {
970
+ // Store cursor context for completion
971
+ if (this.cursorMode) {
972
+ this.stoppedAtCursor = true;
973
+ this.cursorContext = {
974
+ typeBeforeCursor: context.inputType,
975
+ cursorNode: node,
976
+ expectedType: undefined,
977
+ functionCall: undefined
978
+ };
941
979
 
942
- if (allSameType) {
943
- return { ...firstType, singleton: false };
944
- }
945
- }
946
-
947
- // Otherwise, it's a heterogeneous collection
948
- return { type: 'Any', singleton: false };
949
- }
950
-
951
- private async inferTypeCastType(node: TypeCastNode): Promise<TypeInfo> {
952
- const targetType = node.targetType;
953
-
954
- // If we have a model provider, try to get the type
955
- if (this.modelProvider) {
956
- const typeInfo = await this.modelProvider.getType(targetType);
957
- if (typeInfo) {
958
- return typeInfo;
959
- }
960
- }
961
-
962
- // Otherwise, check if it's a FHIRPath primitive type
963
- const fhirPathTypes = ['String', 'Boolean', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity'];
964
- if (fhirPathTypes.includes(targetType)) {
965
- return { type: targetType as TypeName, singleton: true };
966
- }
967
-
968
- return { type: 'Any', singleton: true };
969
- }
970
-
971
- private isTypeCompatible(source: TypeInfo, target: TypeInfo): boolean {
972
- // Exact match
973
- if (source.type === target.type && source.singleton === target.singleton) {
974
- return true;
975
- }
976
-
977
- // Any is compatible with everything
978
- if (source.type === 'Any' || target.type === 'Any') {
979
- return true;
980
- }
981
-
982
- // Singleton can be promoted to collection
983
- if (source.singleton && !target.singleton && source.type === target.type) {
984
- return true;
985
- }
986
-
987
- // Type hierarchy compatibility
988
- if (this.isSubtypeOf(source.type, target.type)) {
989
- // Check singleton compatibility
990
- if (source.singleton === target.singleton || (source.singleton && !target.singleton)) {
991
- return true;
992
- }
993
- }
994
-
995
- // Numeric type compatibility
996
- if (this.isNumericType(source.type) && this.isNumericType(target.type)) {
997
- // Integer can be used where Decimal is expected
998
- if (source.type === 'Integer' && target.type === 'Decimal') {
999
- return source.singleton !== undefined && target.singleton !== undefined &&
1000
- (source.singleton === target.singleton || (source.singleton && !target.singleton));
980
+ // Set expected type based on cursor context
981
+ if (node.context === CursorContext.Index) {
982
+ // Index expects an integer
983
+ this.cursorContext.expectedType = { type: 'Integer', singleton: true };
984
+ } else if (node.context === CursorContext.Argument) {
985
+ // Arguments context - check if we're in a function
986
+ const parent = (node as any).parent;
987
+ if (parent && parent.type === NodeType.Function) {
988
+ const funcNode = parent as FunctionNode;
989
+ if (funcNode.name.type === NodeType.Identifier) {
990
+ const funcName = (funcNode.name as IdentifierNode).name;
991
+ const funcDef = registry.getFunction(funcName);
992
+ if (funcDef) {
993
+ // Find argument index
994
+ const argIndex = funcNode.arguments.indexOf(node);
995
+ this.cursorContext.functionCall = {
996
+ definition: funcDef,
997
+ argumentIndex: argIndex >= 0 ? argIndex : 0
998
+ };
999
+ }
1000
+ }
1001
+ }
1001
1002
  }
1002
1003
  }
1003
1004
 
1004
- return false;
1005
+ return {
1006
+ type: { type: 'Any', singleton: false },
1007
+ diagnostics: []
1008
+ };
1005
1009
  }
1006
-
1007
- private isSubtypeOf(source: TypeName, target: TypeName): boolean {
1008
- // Basic subtyping rules
1009
- if (source === target) return true;
1010
- if (target === 'Any') return true;
1011
-
1012
- // Integer is a subtype of Decimal
1013
- if (source === 'Integer' && target === 'Decimal') return true;
1014
-
1015
- // Model-specific subtyping would be checked via ModelProvider
1016
- // For now, we don't have other subtyping rules
1017
- return false;
1010
+
1011
+ // Legacy union combiner removed; union handled in analyzeBinary
1012
+
1013
+ /**
1014
+ * Helper to create diagnostic errors.
1015
+ */
1016
+ private createError(node: ASTNode, message: string, code?: string): Diagnostic {
1017
+ return {
1018
+ range: node.range,
1019
+ message,
1020
+ severity: DiagnosticSeverity.Error,
1021
+ code,
1022
+ source: 'fhirpath'
1023
+ };
1018
1024
  }
1019
1025
 
1020
- private isNumericType(type: TypeName): boolean {
1021
- return type === 'Integer' || type === 'Decimal' || type === 'Quantity';
1026
+ private createWarning(node: ASTNode, message: string, code?: string): Diagnostic {
1027
+ return {
1028
+ range: node.range,
1029
+ message,
1030
+ severity: DiagnosticSeverity.Warning,
1031
+ code,
1032
+ source: 'fhirpath'
1033
+ };
1022
1034
  }
1023
-
1035
+
1036
+ /**
1037
+ * Helper method to infer TypeInfo from runtime values (used for user variables).
1038
+ */
1024
1039
  private inferValueType(value: any): TypeInfo {
1025
1040
  if (Array.isArray(value)) {
1026
1041
  if (value.length === 0) {
@@ -1034,7 +1049,9 @@ export class Analyzer {
1034
1049
  if (typeof value === 'string') {
1035
1050
  return { type: 'String', singleton: true };
1036
1051
  } else if (typeof value === 'number') {
1037
- return { type: Number.isInteger(value) ? 'Integer' : 'Decimal', singleton: true };
1052
+ return Number.isInteger(value)
1053
+ ? { type: 'Integer', singleton: true }
1054
+ : { type: 'Decimal', singleton: true };
1038
1055
  } else if (typeof value === 'boolean') {
1039
1056
  return { type: 'Boolean', singleton: true };
1040
1057
  } else if (value instanceof Date) {
@@ -1043,359 +1060,55 @@ export class Analyzer {
1043
1060
  return { type: 'Any', singleton: true };
1044
1061
  }
1045
1062
  }
1046
-
1047
- private resolveResultType(
1048
- resultSpec: TypeInfo | 'inputType' | 'leftType' | 'rightType',
1063
+
1064
+ async analyze(
1065
+ ast: ASTNode,
1066
+ userVariables?: Record<string, any>,
1049
1067
  inputType?: TypeInfo,
1050
- leftType?: TypeInfo,
1051
- rightType?: TypeInfo
1052
- ): TypeInfo {
1053
- if (typeof resultSpec !== 'string') {
1054
- return resultSpec;
1055
- }
1056
-
1057
- switch (resultSpec) {
1058
- case 'inputType':
1059
- return inputType || { type: 'Any', singleton: false };
1060
- case 'leftType':
1061
- // For union-like operators, result is always a collection
1062
- return leftType ? { ...leftType, singleton: false } : { type: 'Any', singleton: false };
1063
- case 'rightType':
1064
- return rightType ? { ...rightType, singleton: false } : { type: 'Any', singleton: false };
1065
- default:
1066
- return { type: 'Any', singleton: false };
1067
- }
1068
- }
1069
-
1070
- private checkBinaryOperatorTypes(node: BinaryNode, operator: import('./types').OperatorDefinition): void {
1071
- const leftType = node.left.typeInfo!;
1072
- const rightType = node.right.typeInfo!;
1073
-
1074
- // Find if any signature matches
1075
- let foundMatch = false;
1076
- for (const sig of operator.signatures) {
1077
- if (this.isTypeCompatible(leftType, sig.left) &&
1078
- this.isTypeCompatible(rightType, sig.right)) {
1079
- foundMatch = true;
1080
- break;
1081
- }
1082
- }
1068
+ options?: AnalyzerOptions
1069
+ ): Promise<AnalysisResultWithCursor> {
1070
+ this.cursorMode = options?.cursorMode ?? false;
1071
+ this.stoppedAtCursor = false;
1072
+ this.cursorContext = undefined;
1083
1073
 
1084
- if (!foundMatch) {
1085
- const leftTypeStr = this.typeToString(leftType);
1086
- const rightTypeStr = this.typeToString(rightType);
1087
- this.diagnostics.push(
1088
- toDiagnostic(Errors.operatorTypeMismatch(node.operator, leftTypeStr, rightTypeStr, node.range))
1089
- );
1090
- }
1091
- }
1092
-
1093
- private checkFunctionArgumentTypes(node: FunctionNode, func: import('./types').FunctionDefinition): void {
1094
- const params = func.signatures?.[0]?.parameters || [];
1074
+ // Create initial context with system and user variables
1075
+ const systemVars = new Map<string, TypeInfo>();
1076
+ // $this should be the input type (the root context), not Any
1077
+ systemVars.set('$this', inputType || { type: 'Any', singleton: false });
1078
+ systemVars.set('$index', { type: 'Integer', singleton: true });
1079
+ systemVars.set('$total', { type: 'Any', singleton: false });
1095
1080
 
1096
- for (let i = 0; i < Math.min(node.arguments.length, params.length); i++) {
1097
- const arg = node.arguments[i]!;
1098
- const param = params[i]!;
1099
-
1100
- if (arg.typeInfo && !param.expression) {
1101
- // For non-expression parameters, check type compatibility
1102
- if (!this.isTypeCompatible(arg.typeInfo, param.type)) {
1103
- const argTypeStr = this.typeToString(arg.typeInfo);
1104
- const paramTypeStr = this.typeToString(param.type);
1105
- this.diagnostics.push(
1106
- toDiagnostic(Errors.argumentTypeMismatch(i + 1, func.name, paramTypeStr, argTypeStr, arg.range))
1107
- );
1081
+ const userVars = new Map<string, TypeInfo>();
1082
+ if (userVariables) {
1083
+ Object.keys(userVariables).forEach(name => {
1084
+ const value = userVariables[name];
1085
+ if (value !== undefined && value !== null) {
1086
+ userVars.set(name, this.inferValueType(value));
1108
1087
  }
1109
- }
1110
- }
1111
- }
1112
-
1113
- private typeToString(type: TypeInfo): string {
1114
- const singletonStr = type.singleton ? '' : '[]';
1115
- if (type.namespace && type.name) {
1116
- return `${type.namespace}.${type.name}${singletonStr}`;
1088
+ });
1117
1089
  }
1118
- return `${type.type}${singletonStr}`;
1119
- }
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
1090
 
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
-
1155
- /**
1156
- * Annotate AST with type information
1157
- */
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
- }
1091
+ // Create context with analyzeNode callback and model provider
1092
+ const initialContext = new AnalysisContext(
1093
+ inputType || { type: 'Any', singleton: false },
1094
+ systemVars,
1095
+ userVars,
1096
+ (node, ctx) => this.analyzeNode(node, ctx),
1097
+ this.modelProvider
1098
+ );
1171
1099
 
1172
- // If we've already stopped at cursor, don't continue
1173
- if (this.stoppedAtCursor) {
1174
- return;
1175
- }
1100
+ // Run context-flow analysis
1101
+ const result = await this.analyzeNode(ast, initialContext);
1176
1102
 
1177
- // Handle error nodes
1178
- if (node.type === 'Error') {
1179
- const errorNode = node as ErrorNode;
1180
- // Infer a reasonable type for error nodes
1181
- node.typeInfo = this.inferErrorNodeType(errorNode, inputType);
1182
- // Add diagnostic for the error
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
- });
1190
- return;
1191
- }
1103
+ // Legacy annotateAST/visitor path removed from default analysis to avoid duplication.
1192
1104
 
1193
- // Infer and attach type info
1194
- node.typeInfo = await this.inferType(node, inputType);
1195
-
1196
- // Recursively annotate children
1197
- switch (node.type) {
1198
- case NodeType.Binary:
1199
- const binaryNode = node as BinaryNode;
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
- }
1219
-
1220
- // For navigation, pass the left's type as input to the right
1221
- if (binaryNode.operator === '.') {
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
- }
1252
- } else {
1253
- await this.annotateAST(binaryNode.right, inputType);
1254
- }
1255
- break;
1256
-
1257
- case NodeType.Unary:
1258
- const unaryNode = node as UnaryNode;
1259
- await this.annotateAST(unaryNode.operand, inputType);
1260
- break;
1261
-
1262
- case NodeType.Function:
1263
- const funcNode = node as FunctionNode;
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
- }
1368
- break;
1369
-
1370
- case NodeType.Collection:
1371
- const collNode = node as CollectionNode;
1372
- for (const el of collNode.elements) {
1373
- await this.annotateAST(el, inputType);
1374
- if (this.stoppedAtCursor) break;
1375
- }
1376
- break;
1377
-
1378
- case NodeType.TypeCast:
1379
- const castNode = node as TypeCastNode;
1380
- await this.annotateAST(castNode.expression, inputType);
1381
- break;
1382
-
1383
- case NodeType.MembershipTest:
1384
- const memberNode = node as MembershipTestNode;
1385
- await this.annotateAST(memberNode.expression, inputType);
1386
- break;
1387
-
1388
- case NodeType.Index:
1389
- const indexNode = node as IndexNode;
1390
- await this.annotateAST(indexNode.expression, inputType);
1391
- if (!this.stoppedAtCursor) {
1392
- await this.annotateAST(indexNode.index, inputType);
1393
- }
1394
- break;
1395
-
1396
- case NodeType.TypeOrIdentifier:
1397
- // TypeOrIdentifier doesn't have children to annotate
1398
- break;
1399
- }
1105
+ return {
1106
+ diagnostics: result.diagnostics,
1107
+ ast,
1108
+ type: result.type,
1109
+ userVariables: new Map(result.context?.userVariables || initialContext.userVariables),
1110
+ stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
1111
+ cursorContext: this.cursorMode ? this.cursorContext : undefined
1112
+ };
1400
1113
  }
1401
- }
1114
+ }