@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900

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