@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
@@ -8,7 +8,6 @@ import type {
8
8
  VariableNode,
9
9
  CollectionNode,
10
10
  IndexNode,
11
- TypeOrIdentifierNode,
12
11
  MembershipTestNode,
13
12
  TypeCastNode,
14
13
  QuantityNode
@@ -16,152 +15,22 @@ import type {
16
15
  import { NodeType } from './types';
17
16
  import { Registry } from './registry';
18
17
  import * as operations from './operations';
19
- import type { EvaluationResult, FunctionEvaluator, NodeEvaluator, OperationEvaluator, RuntimeContext } from './types';
20
- import { createQuantity } from './quantity-value';
21
- import { box, unbox, ensureBoxed, type FHIRPathValue } from './boxing';
18
+ import type { EvaluationResult, FunctionEvaluator, NodeEvaluator, OperationEvaluator, RuntimeContext, TypeInfo } from './types';
19
+ import { createQuantity } from './complex-types/quantity-value';
20
+ import { box, unbox, ensureBoxed, type FHIRPathValue } from './interpreter/boxing';
22
21
  import { Errors } from './errors';
22
+ import { detectChoiceValues, getPrimitiveElement, maybeParseTemporal, reboxResource } from './interpreter/navigator';
23
+ import { RuntimeContextManager } from './interpreter/runtime-context';
24
+ import { Analyzer } from './analyzer';
25
+ import { DiagnosticSeverity } from './types';
26
+ import { FHIRPathError, ErrorCodes } from './errors';
27
+ import { toTemporalString } from './complex-types/temporal';
23
28
 
24
29
  /**
25
30
  * Runtime context manager that provides efficient prototype-based context operations
26
31
  * for both interpreter and compiler.
27
32
  */
28
- export class RuntimeContextManager {
29
- /**
30
- * Create a new runtime context
31
- */
32
- static create(input: any[], initialVariables?: Record<string, any>): RuntimeContext {
33
- const context = Object.create(null) as RuntimeContext;
34
-
35
- context.input = input;
36
- context.focus = input;
37
-
38
- // Create variables object with null prototype to avoid pollution
39
- context.variables = Object.create(null);
40
-
41
- // Set root context variables with % prefix
42
- context.variables['%context'] = input;
43
- context.variables['%resource'] = input;
44
- context.variables['%rootResource'] = input;
45
-
46
- // Add any initial variables (with % prefix for user-defined)
47
- if (initialVariables) {
48
- for (const [key, value] of Object.entries(initialVariables)) {
49
- // Add % prefix if not already present and not a special variable
50
- const varKey = key.startsWith('$') || key.startsWith('%') ? key : `%${key}`;
51
- context.variables[varKey] = value;
52
- }
53
- }
54
-
55
- return context;
56
- }
57
-
58
- /**
59
- * Create a child context using prototype inheritance
60
- * O(1) operation - no copying needed
61
- */
62
- static copy(context: RuntimeContext): RuntimeContext {
63
- // Create child context with parent as prototype
64
- const newContext = Object.create(context) as RuntimeContext;
65
-
66
- // Create child variables that inherit from parent's variables
67
- newContext.variables = Object.create(context.variables);
68
-
69
- // input and focus are inherited through prototype chain
70
- // Only set them if they need to change
71
-
72
- return newContext;
73
- }
74
-
75
- /**
76
- * Create a new context with updated input/focus
77
- */
78
- static withInput(context: RuntimeContext, input: any[], focus?: any[]): RuntimeContext {
79
- const newContext = this.copy(context);
80
- newContext.input = input;
81
- newContext.focus = focus ?? input;
82
- return newContext;
83
- }
84
-
85
- /**
86
- * Set iterator context ($this, $index)
87
- */
88
- static withIterator(
89
- context: RuntimeContext,
90
- item: any,
91
- index: number
92
- ): RuntimeContext {
93
- let newContext = this.setVariable(context, '$this', [item], true);
94
- newContext = this.setVariable(newContext, '$index', index, true);
95
- return newContext;
96
- }
97
-
98
- /**
99
- * Set a variable in the context (handles both special $ and user % variables)
100
- */
101
- static setVariable(context: RuntimeContext, name: string, value: any, allowRedefinition: boolean = false): RuntimeContext {
102
- // Ensure value is array for consistency (except for special variables like $index)
103
- const arrayValue = (name === '$index' || name === '$total') ? value :
104
- Array.isArray(value) ? value : [value];
105
-
106
- // Determine variable key based on prefix
107
- let varKey = name;
108
- if (!name.startsWith('$') && !name.startsWith('%')) {
109
- // No prefix - assume user-defined variable, add % prefix
110
- varKey = `%${name}`;
111
- }
112
-
113
- // Check for system variables (with or without % prefix)
114
- const systemVariables = ['context', 'resource', 'rootResource', 'ucum', 'sct', 'loinc'];
115
- const baseVarName = varKey.startsWith('%') ? varKey.substring(1) : varKey;
116
- if (systemVariables.includes(baseVarName)) {
117
- // Silently return original context for system variable redefinition
118
- return context;
119
- }
120
-
121
- // Check if variable already exists (unless redefinition is allowed)
122
- if (!allowRedefinition && context.variables && Object.prototype.hasOwnProperty.call(context.variables, varKey)) {
123
- // Silently return original context for variable redefinition
124
- return context;
125
- }
126
-
127
- // Create new context and set variable
128
- const newContext = this.copy(context);
129
- newContext.variables[varKey] = arrayValue;
130
-
131
- // Special handling for $this
132
- if (varKey === '$this' && Array.isArray(arrayValue) && arrayValue.length === 1) {
133
- newContext.input = arrayValue;
134
- newContext.focus = arrayValue;
135
- }
136
-
137
- return newContext;
138
- }
139
-
140
- /**
141
- * Get a variable from context
142
- */
143
- static getVariable(context: RuntimeContext, name: string): any | undefined {
144
- // Handle special cases
145
- if (name === '$this' || name === '$index' || name === '$total') {
146
- return context.variables[name];
147
- }
148
-
149
- // Handle environment variables (with or without % prefix)
150
- if (name === 'context' || name === '%context') {
151
- return context.variables['%context'];
152
- }
153
- if (name === 'resource' || name === '%resource') {
154
- return context.variables['%resource'];
155
- }
156
- if (name === 'rootResource' || name === '%rootResource') {
157
- return context.variables['%rootResource'];
158
- }
159
-
160
- // Handle user-defined variables (add % prefix if not present)
161
- const varKey = name.startsWith('%') ? name : `%${name}`;
162
- return context.variables[varKey];
163
- }
164
- }
33
+ // RuntimeContextManager moved to './runtime-context'
165
34
 
166
35
  export class Interpreter {
167
36
  private registry: Registry;
@@ -179,8 +48,8 @@ export class Interpreter {
179
48
  // Initialize node evaluators using object dispatch pattern
180
49
  this.nodeEvaluators = {
181
50
  [NodeType.Literal]: this.evaluateLiteral.bind(this),
51
+ [NodeType.TemporalLiteral]: this.evaluateTemporalLiteral.bind(this),
182
52
  [NodeType.Identifier]: this.evaluateIdentifier.bind(this),
183
- [NodeType.TypeOrIdentifier]: this.evaluateTypeOrIdentifier.bind(this),
184
53
  [NodeType.Binary]: this.evaluateBinary.bind(this),
185
54
  [NodeType.Unary]: this.evaluateUnary.bind(this),
186
55
  [NodeType.Function]: this.evaluateFunction.bind(this),
@@ -256,226 +125,329 @@ export class Interpreter {
256
125
  return context;
257
126
  }
258
127
 
128
+ /**
129
+ * Parse, analyze and evaluate a FHIRPath expression with optional
130
+ * model provider, variables and input type. Returns unboxed values
131
+ * with temporal values formatted as FHIRPath literals.
132
+ */
133
+ async evaluateExpression(
134
+ expression: string,
135
+ options: {
136
+ input?: unknown;
137
+ variables?: Record<string, unknown>;
138
+ inputType?: TypeInfo;
139
+ modelProvider?: import('./types').ModelProvider;
140
+ now?: Date;
141
+ } = {}
142
+ ): Promise<any[]> {
143
+ // Analyze expression first (ensures type info and diagnostics)
144
+ const analysis = await Analyzer.analyzeExpression(expression, {
145
+ variables: options.variables,
146
+ modelProvider: options.modelProvider ?? this.modelProvider,
147
+ inputType: options.inputType,
148
+ errorRecovery: false,
149
+ });
150
+
151
+ const errors = analysis.diagnostics.filter(d => d.severity === DiagnosticSeverity.Error);
152
+ if (errors.length > 0) {
153
+ const first = errors[0]!;
154
+ const code = typeof first.code === 'string' && first.code.length > 0 ? first.code : ErrorCodes.INVALID_OPERATION;
155
+ throw new FHIRPathError(code, first.message, first.range);
156
+ }
157
+
158
+ // Bootstrap runtime context and boxed input
159
+ const { context, input } = await RuntimeContextManager.bootstrapContext(options.input, {
160
+ modelProvider: options.modelProvider ?? this.modelProvider,
161
+ variables: options.variables,
162
+ now: options.now,
163
+ });
164
+
165
+ // Evaluate using analyzed AST and BOXED input
166
+ const result = await this.evaluate(analysis.ast, input as any[], context);
167
+
168
+ // Unbox and format temporal outputs for API parity
169
+ return result.value.map((boxedValue) => {
170
+ const value = unbox(boxedValue);
171
+ if (value && typeof value === 'object' && 'kind' in value) {
172
+ if ((value as any).kind === 'FHIRDate' || (value as any).kind === 'FHIRDateTime') {
173
+ return '@' + toTemporalString(value as any);
174
+ } else if ((value as any).kind === 'FHIRTime') {
175
+ return '@T' + toTemporalString(value as any);
176
+ }
177
+ }
178
+ return value;
179
+ });
180
+ }
181
+
182
+ // Helper: classify a boolean operand for short-circuit decisions
183
+ private getBooleanKind(values: FHIRPathValue[]): 'empty' | 'true' | 'false' | 'other' {
184
+ if (values.length === 0) {
185
+ return 'empty';
186
+ }
187
+ const v = unbox(values[0]!);
188
+ if (v === true) {
189
+ return 'true';
190
+ }
191
+ if (v === false) {
192
+ return 'false';
193
+ }
194
+ return 'other';
195
+ }
196
+
197
+ private boxBoolean(b: boolean): FHIRPathValue {
198
+ return box(b, { type: 'Boolean', singleton: true });
199
+ }
200
+
201
+ // TemporalLiteral node evaluator
202
+ private async evaluateTemporalLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
203
+ const temporal = node as import('./types').TemporalLiteralNode;
204
+
205
+ // The value is already parsed in the parser
206
+ let typeInfo: import('./types').TypeInfo;
207
+
208
+ if (temporal.valueType === 'date') {
209
+ typeInfo = { type: 'Date', singleton: true };
210
+ } else if (temporal.valueType === 'datetime') {
211
+ typeInfo = { type: 'DateTime', singleton: true };
212
+ } else {
213
+ typeInfo = { type: 'Time', singleton: true };
214
+ }
215
+
216
+ return {
217
+ value: [box(temporal.value, typeInfo)],
218
+ context
219
+ };
220
+ }
221
+
259
222
  // Literal node evaluator
260
223
  private async evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
261
224
  const literal = node as LiteralNode;
262
225
 
263
226
  // Box the literal value with appropriate type info
264
227
  let typeInfo: import('./types').TypeInfo | undefined;
265
- const value = literal.value;
228
+ let value: any = literal.value;
266
229
 
267
- if (typeof value === 'string') {
230
+ // Handle temporal literals (backwards compatibility - should not reach here with new parser)
231
+ if (literal.valueType === 'date' || literal.valueType === 'datetime' || literal.valueType === 'time') {
232
+ // Import temporal parsing function
233
+ const { parseTemporalLiteral } = await import('./complex-types/temporal');
234
+ // Parse the temporal literal (add @ back since it was stripped by parser)
235
+ const temporalValue = parseTemporalLiteral('@' + literal.value);
236
+
237
+ // Set appropriate type info
238
+ if (literal.valueType === 'date') {
239
+ typeInfo = { type: 'Date', singleton: true };
240
+ } else if (literal.valueType === 'datetime') {
241
+ typeInfo = { type: 'DateTime', singleton: true };
242
+ } else if (literal.valueType === 'time') {
243
+ typeInfo = { type: 'Time', singleton: true };
244
+ }
245
+
246
+ value = temporalValue;
247
+ } else if (typeof value === 'string') {
268
248
  typeInfo = { type: 'String', singleton: true };
269
249
  } else if (typeof value === 'number') {
270
- typeInfo = Number.isInteger(value) ?
271
- { type: 'Integer', singleton: true } :
272
- { type: 'Decimal', singleton: true };
250
+ // Use the valueType from the literal node to determine if it's integer or decimal
251
+ // This preserves the distinction between 1.0 (decimal) and 1 (integer)
252
+ typeInfo = literal.valueType === 'decimal' ?
253
+ { type: 'Decimal', singleton: true } :
254
+ { type: 'Integer', singleton: true };
273
255
  } else if (typeof value === 'boolean') {
274
256
  typeInfo = { type: 'Boolean', singleton: true };
275
257
  }
276
258
 
277
259
  return {
278
- value: [box(literal.value, typeInfo)],
260
+ value: [box(value, typeInfo)],
279
261
  context
280
262
  };
281
263
  }
282
264
 
283
- // Identifier node evaluator
284
- private async evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
285
- const identifier = node as IdentifierNode;
286
- const name = identifier.name;
265
+ // Helper: Handle extension elements
266
+ private handleExtension(
267
+ boxedItem: FHIRPathValue,
268
+ nodeTypeInfo?: TypeInfo
269
+ ): FHIRPathValue[] {
270
+ const results: FHIRPathValue[] = [];
271
+ if (boxedItem.primitiveElement?.extension) {
272
+ for (const ext of boxedItem.primitiveElement.extension) {
273
+ results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
274
+ }
275
+ }
276
+ return results;
277
+ }
287
278
 
288
- // Navigate property on each boxed item in input
279
+ // Helper: Handle FHIR choice types (e.g., value[x])
280
+ private async handleChoiceTypes(
281
+ item: object,
282
+ name: string,
283
+ context: RuntimeContext
284
+ ): Promise<FHIRPathValue[]> {
289
285
  const results: FHIRPathValue[] = [];
290
-
291
- // Get the type info from the node (set by analyzer)
292
- const nodeTypeInfo = node.typeInfo;
293
-
294
- for (const boxedItem of input) {
295
- const item = unbox(boxedItem);
296
-
297
- // Special handling for primitive extension navigation
298
- if (name === 'extension' && boxedItem.primitiveElement?.extension) {
299
- // Navigation from a primitive value to its extensions
300
- for (const ext of boxedItem.primitiveElement.extension) {
301
- results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
286
+ const choiceHits = await detectChoiceValues(item as Record<string, unknown>, name, context.modelProvider);
287
+ for (const hit of choiceHits) {
288
+ results.push(box(hit.value, hit.typeInfo, hit.primitiveElement));
289
+ }
290
+ return results;
291
+ }
292
+
293
+ // Helper: Handle union type choices
294
+ private handleUnionChoices(
295
+ item: object,
296
+ nodeTypeInfo?: TypeInfo
297
+ ): FHIRPathValue[] {
298
+ const results: FHIRPathValue[] = [];
299
+ if (
300
+ nodeTypeInfo?.modelContext &&
301
+ typeof nodeTypeInfo.modelContext === 'object' &&
302
+ 'isUnion' in nodeTypeInfo.modelContext &&
303
+ (nodeTypeInfo.modelContext as any).isUnion &&
304
+ 'choices' in nodeTypeInfo.modelContext &&
305
+ Array.isArray((nodeTypeInfo.modelContext as any).choices)
306
+ ) {
307
+ for (const choice of (nodeTypeInfo.modelContext as any).choices) {
308
+ const choiceName = choice.choiceName;
309
+ if (choiceName && choiceName in (item as any)) {
310
+ const value = (item as any)[choiceName];
311
+ const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, choiceName);
312
+ const choiceTypeInfo = { type: choice.type, singleton: !Array.isArray(value), modelContext: choice } as any;
313
+ if (Array.isArray(value)) {
314
+ for (const v of value) {
315
+ results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
316
+ }
317
+ } else if (value !== null && value !== undefined) {
318
+ results.push(box(value, choiceTypeInfo, primitiveElement));
319
+ }
302
320
  }
303
- continue;
304
321
  }
305
-
306
- if (item && typeof item === 'object') {
307
- // First, check if this might be a FHIR choice type by looking for choice properties
308
- // FHIR choice types use the pattern: base name + Type suffix (e.g., valueQuantity, valueCodeableConcept)
309
- let foundChoiceValue = false;
322
+ }
323
+ return results;
324
+ }
325
+
326
+ // Helper: Handle standard property access
327
+ private async handleStandardProperty(
328
+ item: object,
329
+ name: string,
330
+ nodeTypeInfo: TypeInfo | undefined,
331
+ context: RuntimeContext,
332
+ parentTypeInfo?: TypeInfo
333
+ ): Promise<FHIRPathValue[]> {
334
+ const results: FHIRPathValue[] = [];
335
+ if (name in (item as any)) {
336
+ const value = (item as any)[name];
337
+ const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, name);
338
+
339
+ // Determine if this is a FHIR primitive - if parent is a FHIR resource and value is primitive
340
+ const isFHIRPrimitive = parentTypeInfo &&
341
+ parentTypeInfo.type &&
342
+ parentTypeInfo.type !== 'Any' &&
343
+ !parentTypeInfo.type.startsWith('System.') &&
344
+ (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number');
345
+
346
+ if (Array.isArray(value)) {
347
+ let elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
310
348
 
311
- // Check for FHIR choice type pattern
312
- if (context.modelProvider) {
313
- // Try common FHIR choice patterns
314
- const possibleChoiceProperties = Object.keys(item).filter(key =>
315
- key.startsWith(name) && key !== name && key.length > name.length
316
- );
317
-
318
- if (possibleChoiceProperties.length > 0) {
319
- // This looks like a choice type - return all matching values
320
- for (const choiceProp of possibleChoiceProperties) {
321
- const value = item[choiceProp];
322
- const primitiveElementName = `_${choiceProp}`;
323
- const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
324
-
325
- // Try to determine the type from the property name suffix
326
- let choiceType = 'Any';
327
- const suffix = choiceProp.substring(name.length);
328
- if (suffix) {
329
- // Remove leading uppercase letter and make it the type
330
- choiceType = suffix;
331
- }
332
-
333
- if (Array.isArray(value)) {
334
- for (const v of value) {
335
- // For FHIR resources, use their resourceType
336
- if (v && typeof v === 'object' && 'resourceType' in v) {
337
- const typeInfo = await context.modelProvider!.getType(v.resourceType);
338
- results.push(box(v, typeInfo || { type: v.resourceType as any, singleton: true }, primitiveElement));
339
- } else {
340
- results.push(box(v, { type: choiceType as any, singleton: true }, primitiveElement));
341
- }
342
- }
343
- } else if (value !== null && value !== undefined) {
344
- // For FHIR resources, use their resourceType
345
- if (value && typeof value === 'object' && 'resourceType' in value) {
346
- const typeInfo = await context.modelProvider!.getType(value.resourceType);
347
- results.push(box(value, typeInfo || { type: value.resourceType as any, singleton: true }, primitiveElement));
348
- } else {
349
- results.push(box(value, { type: choiceType as any, singleton: !Array.isArray(value) }, primitiveElement));
350
- }
351
- }
352
- foundChoiceValue = true;
353
- }
349
+ // For FHIR primitives, use FHIR namespace
350
+ if (isFHIRPrimitive && elementTypeInfo) {
351
+ if (elementTypeInfo.type === 'Boolean') {
352
+ elementTypeInfo = { ...elementTypeInfo, type: 'boolean' as any };
354
353
  }
355
354
  }
356
355
 
357
- // Check if this is a choice type navigation from analyzer
358
- if (!foundChoiceValue && nodeTypeInfo?.modelContext && typeof nodeTypeInfo.modelContext === 'object' &&
359
- 'isUnion' in nodeTypeInfo.modelContext &&
360
- nodeTypeInfo.modelContext.isUnion && 'choices' in nodeTypeInfo.modelContext &&
361
- Array.isArray(nodeTypeInfo.modelContext.choices)) {
362
- // For choice types, look for any of the choice properties
363
- for (const choice of nodeTypeInfo.modelContext.choices) {
364
- const choiceName = choice.choiceName;
365
- if (choiceName && choiceName in item) {
366
- const value = item[choiceName];
367
- const primitiveElementName = `_${choiceName}`;
368
- const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
369
-
370
- // Box with the specific choice type
371
- const choiceTypeInfo = {
372
- type: choice.type,
373
- singleton: !Array.isArray(value),
374
- modelContext: choice
375
- };
376
-
377
- if (Array.isArray(value)) {
378
- for (const v of value) {
379
- results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
380
- }
381
- } else if (value !== null && value !== undefined) {
382
- results.push(box(value, choiceTypeInfo, primitiveElement));
383
- }
384
- foundChoiceValue = true;
385
- }
356
+ for (const v of value) {
357
+ if (
358
+ v && typeof v === 'object' && 'resourceType' in (v as any) && typeof (v as any).resourceType === 'string'
359
+ ) {
360
+ // Always re-box FHIR resources to get proper type information from ModelProvider
361
+ const boxed = await reboxResource(v, true, context.modelProvider);
362
+ results.push(boxed);
363
+ } else {
364
+ const val = await maybeParseTemporal(v, elementTypeInfo, context.modelProvider);
365
+ results.push(box(val, elementTypeInfo, primitiveElement));
386
366
  }
387
367
  }
388
-
389
- if (!foundChoiceValue && name in item) {
390
- // Regular property navigation
391
- const value = item[name];
392
- const primitiveElementName = `_${name}`;
393
- const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
394
-
395
- if (Array.isArray(value)) {
396
- // Box each array element with type info
397
- // For arrays, make the type singleton since each element is a single value
398
- const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
399
- for (const v of value) {
400
- // Special handling for FHIR resources - use their resourceType
401
- // Do this when the property could be polymorphic (type is 'Any' or 'Resource')
402
- if (v && typeof v === 'object' && 'resourceType' in v && typeof v.resourceType === 'string' &&
403
- (!elementTypeInfo || elementTypeInfo.type === 'Any' || (elementTypeInfo as any).type === 'Resource')) {
404
- // Get full type info from model provider if available
405
- let resourceTypeInfo;
406
- if (context.modelProvider) {
407
- resourceTypeInfo = await context.modelProvider.getType(v.resourceType);
408
- if (resourceTypeInfo) {
409
- // Make it singleton since it's a single element in the array
410
- resourceTypeInfo = { ...resourceTypeInfo, singleton: true };
411
- }
412
- }
413
- if (!resourceTypeInfo) {
414
- // Fallback to basic type info
415
- resourceTypeInfo = {
416
- type: v.resourceType as import('./types').TypeName,
417
- singleton: true
418
- };
419
- }
420
- results.push(box(v, resourceTypeInfo, primitiveElement));
421
- } else {
422
- results.push(box(v, elementTypeInfo, primitiveElement));
423
- }
424
- }
425
- } else if (value !== null && value !== undefined) {
426
- // Special handling for FHIR resources - use their resourceType
427
- // Do this when the property could be polymorphic (type is 'Any' or 'Resource')
428
- if (value && typeof value === 'object' && 'resourceType' in value && typeof value.resourceType === 'string' &&
429
- (!nodeTypeInfo || nodeTypeInfo.type === 'Any' || (nodeTypeInfo as any).type === 'Resource')) {
430
- // Get full type info from model provider if available
431
- let resourceTypeInfo;
432
- if (context.modelProvider) {
433
- resourceTypeInfo = await context.modelProvider.getType(value.resourceType);
434
- if (resourceTypeInfo) {
435
- // Preserve singleton status
436
- resourceTypeInfo = { ...resourceTypeInfo, singleton: !Array.isArray(value) };
437
- }
438
- }
439
- if (!resourceTypeInfo) {
440
- // Fallback to basic type info
441
- resourceTypeInfo = {
442
- type: value.resourceType as import('./types').TypeName,
443
- singleton: !Array.isArray(value)
444
- };
445
- }
446
- results.push(box(value, resourceTypeInfo, primitiveElement));
447
- } else {
448
- // Box single value with primitive element if available
449
- results.push(box(value, nodeTypeInfo, primitiveElement));
368
+ } else if (value !== null && value !== undefined) {
369
+ if (
370
+ value && typeof value === 'object' && 'resourceType' in (value as any) && typeof (value as any).resourceType === 'string'
371
+ ) {
372
+ // Always re-box FHIR resources to get proper type information from ModelProvider
373
+ const boxed = await reboxResource(value, true, context.modelProvider);
374
+ results.push(boxed);
375
+ } else {
376
+ // For FHIR primitives, use FHIR namespace
377
+ let typeInfo = nodeTypeInfo;
378
+ if (isFHIRPrimitive && typeInfo) {
379
+ if (typeInfo.type === 'Boolean') {
380
+ typeInfo = { ...typeInfo, type: 'boolean' as any };
450
381
  }
451
382
  }
383
+
384
+ const val = await maybeParseTemporal(value, typeInfo, context.modelProvider);
385
+ results.push(box(val, typeInfo, primitiveElement));
452
386
  }
453
387
  }
454
388
  }
455
-
456
- return {
457
- value: results,
458
- context
459
- };
389
+ return results;
460
390
  }
461
391
 
462
- // TypeOrIdentifier node evaluator (handles Patient, Observation, etc.)
463
- private async evaluateTypeOrIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
464
- const typeOrId = node as TypeOrIdentifierNode;
465
- const name = typeOrId.name;
392
+ // Identifier node evaluator
393
+ private async evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
394
+ const identifier = node as IdentifierNode;
395
+ const name = identifier.name;
396
+ const nodeTypeInfo = node.typeInfo;
397
+ const results: FHIRPathValue[] = [];
466
398
 
467
- // First try as type filter
468
- const filtered = input.filter(boxedItem => {
399
+ for (const boxedItem of input) {
469
400
  const item = unbox(boxedItem);
470
- return item && typeof item === 'object' && item.resourceType === name;
471
- });
472
401
 
473
- if (filtered.length > 0) {
474
- return { value: filtered, context };
402
+ // 1. Handle extension special case
403
+ if (name === 'extension') {
404
+ results.push(...this.handleExtension(boxedItem, nodeTypeInfo));
405
+ continue;
406
+ }
407
+
408
+ // Process only objects
409
+ if (item && typeof item === 'object') {
410
+ // 2. Handle FHIR choice types (e.g., value[x])
411
+ const choiceResults = await this.handleChoiceTypes(item, name, context);
412
+ if (choiceResults.length > 0) {
413
+ results.push(...choiceResults);
414
+ continue;
415
+ }
416
+
417
+ // 3. Handle union type choices
418
+ const unionResults = this.handleUnionChoices(item, nodeTypeInfo);
419
+ results.push(...unionResults);
420
+
421
+ // 4. Handle standard property access
422
+ const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context, boxedItem.typeInfo);
423
+ results.push(...propertyResults);
424
+ }
475
425
  }
476
426
 
477
- // Otherwise treat as identifier
478
- return await this.evaluateIdentifier(node, input, context);
427
+ // If no properties matched, try type-filter fallback on resources
428
+ if (results.length === 0) {
429
+ const filtered: FHIRPathValue[] = [];
430
+ for (const boxedItem of input) {
431
+ const item = unbox(boxedItem);
432
+ if (item && typeof item === 'object' && (item as any).resourceType === name) {
433
+ if (context.modelProvider) {
434
+ const typeInfo = await context.modelProvider.getType(name);
435
+ if (typeInfo) {
436
+ filtered.push(box(item, { ...typeInfo, singleton: true }));
437
+ } else {
438
+ filtered.push(boxedItem);
439
+ }
440
+ } else {
441
+ filtered.push(boxedItem);
442
+ }
443
+ }
444
+ }
445
+ if (filtered.length > 0) {
446
+ return { value: filtered, context };
447
+ }
448
+ }
449
+
450
+ return { value: results, context };
479
451
  }
480
452
 
481
453
  // Binary operator evaluator
@@ -485,8 +457,29 @@ export class Interpreter {
485
457
 
486
458
  // Special handling for dot operator (sequential pipeline)
487
459
  if (operator === '.') {
460
+ // Check if this is actually a namespaced type in an 'is' expression
461
+ // Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
462
+ if (binary.left.type === NodeType.MembershipTest && binary.right.type === NodeType.Identifier) {
463
+ const membershipTest = binary.left as MembershipTestNode;
464
+ const rightIdent = binary.right as IdentifierNode;
465
+
466
+ // Extract the expression from the membership test
467
+ const expr = membershipTest.expression;
468
+ const typeName = `${membershipTest.targetType}.${rightIdent.name}`;
469
+
470
+ // Evaluate the expression
471
+ const exprResult = await this.evaluate(expr, input, context);
472
+
473
+ // Now apply the is operator with the full type name
474
+ const evaluator = this.operationEvaluators.get('is');
475
+ if (evaluator) {
476
+ return await evaluator(input, context, exprResult.value, [typeName]);
477
+ }
478
+ }
479
+
488
480
  // Evaluate left with current input/context
489
481
  const leftResult = await this.evaluate(binary.left, input, context);
482
+
490
483
  // Use left's output as right's input, and left's context flows to right
491
484
  return await this.evaluate(binary.right, leftResult.value, leftResult.context);
492
485
  }
@@ -495,28 +488,120 @@ export class Interpreter {
495
488
  if (operator === '|') {
496
489
  // Each side of union should have its own variable scope
497
490
  // Variables defined on left side should not be visible on right side
498
- const leftResult = await this.evaluate(binary.left, input, context);
499
- const rightResult = await this.evaluate(binary.right, input, context); // Use original context, not leftResult.context
491
+ // Evaluate both sides in parallel since both use the same input/context
492
+ const [leftResult, rightResult] = await Promise.all([
493
+ this.evaluate(binary.left, input, context),
494
+ this.evaluate(binary.right, input, context)
495
+ ]);
500
496
 
501
497
  // Merge the results
502
- const unionEvaluator = this.operationEvaluators.get('union');
498
+ const unionEvaluator = this.operationEvaluators.get('|');
503
499
  if (unionEvaluator) {
504
500
  return await unionEvaluator(input, context, leftResult.value, rightResult.value);
505
501
  }
506
-
507
- // Fallback if union evaluator not found
508
- return {
509
- value: [...leftResult.value, ...rightResult.value],
510
- context // Original context preserved
511
- };
502
+ // If union evaluator not found, surface a clear error
503
+ throw Errors.noEvaluatorFound('binary operator', '|');
512
504
  }
513
505
 
506
+ // Special handling for 'is' and 'as' operators - right side is a type identifier, not an expression
507
+ if (operator === 'is' || operator === 'as') {
508
+ const leftResult = await this.evaluate(binary.left, input, context);
509
+
510
+ // Extract type name from right side WITHOUT evaluating it
511
+ let typeName: string;
512
+ if (binary.right.type === NodeType.Identifier) {
513
+ typeName = (binary.right as any).name;
514
+ } else if (binary.right.type === NodeType.Binary && (binary.right as any).operator === '.') {
515
+ // Handle namespaced types like System.Boolean or FHIR.Patient
516
+ const rightBinary = binary.right as any;
517
+ if (rightBinary.left.type === NodeType.Identifier && rightBinary.right.type === NodeType.Identifier) {
518
+ typeName = `${rightBinary.left.name}.${rightBinary.right.name}`;
519
+ } else {
520
+ throw new Error('is operator requires a type name as right operand');
521
+ }
522
+ } else {
523
+ throw new Error('is operator requires a type name as right operand');
524
+ }
525
+
526
+ const evaluator = this.operationEvaluators.get('is');
527
+ if (evaluator) {
528
+ return await evaluator(input, context, leftResult.value, [typeName]);
529
+ }
530
+ }
531
+
514
532
  // Get operation evaluator
515
533
  const evaluator = this.operationEvaluators.get(operator);
516
534
  if (evaluator) {
517
- // Most operators evaluate arguments in parallel with same input/context
518
- const leftResult = await this.evaluate(binary.left, input, context);
519
- const rightResult = await this.evaluate(binary.right, input, context);
535
+ // Short-circuit for logical operators when possible
536
+ if (operator === 'and' || operator === 'or' || operator === 'implies') {
537
+ const leftResult = await this.evaluate(binary.left, input, context);
538
+ const kind = this.getBooleanKind(leftResult.value);
539
+
540
+ if (operator === 'and') {
541
+ // false and _ -> false (short-circuit)
542
+ if (kind === 'false') {
543
+ return { value: [this.boxBoolean(false)], context };
544
+ }
545
+ // true and y -> y; empty and false -> false handled by evaluator; need right
546
+ const rightResult = await this.evaluate(binary.right, input, context);
547
+ const operatorDef = this.registry.getOperatorDefinition(operator);
548
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
549
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
550
+ return { value: [], context };
551
+ }
552
+ }
553
+ return await evaluator(input, context, leftResult.value, rightResult.value);
554
+ }
555
+
556
+ if (operator === 'or') {
557
+ // true or _ -> true (short-circuit)
558
+ if (kind === 'true') {
559
+ return { value: [this.boxBoolean(true)], context };
560
+ }
561
+ // false or y -> y; empty or true -> true handled by evaluator; need right
562
+ const rightResult = await this.evaluate(binary.right, input, context);
563
+ const operatorDef = this.registry.getOperatorDefinition(operator);
564
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
565
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
566
+ return { value: [], context };
567
+ }
568
+ }
569
+ return await evaluator(input, context, leftResult.value, rightResult.value);
570
+ }
571
+
572
+ if (operator === 'implies') {
573
+ // false implies _ -> true (short-circuit)
574
+ if (kind === 'false') {
575
+ return { value: [this.boxBoolean(true)], context };
576
+ }
577
+ // true implies y -> y; empty implies y -> true if y true else empty; need right
578
+ const rightResult = await this.evaluate(binary.right, input, context);
579
+ const operatorDef = this.registry.getOperatorDefinition(operator);
580
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
581
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
582
+ return { value: [], context };
583
+ }
584
+ }
585
+ return await evaluator(input, context, leftResult.value, rightResult.value);
586
+ }
587
+ }
588
+
589
+ // Default path: evaluate both operands in parallel
590
+ const [leftResult, rightResult] = await Promise.all([
591
+ this.evaluate(binary.left, input, context),
592
+ this.evaluate(binary.right, input, context)
593
+ ]);
594
+
595
+ // Handle empty propagation for operators
596
+ // Rely exclusively on registry metadata
597
+ const operatorDef = this.registry.getOperatorDefinition(operator);
598
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
599
+ // Check if either operand is empty
600
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
601
+ return { value: [], context };
602
+ }
603
+ }
604
+
520
605
  return await evaluator(input, context, leftResult.value, rightResult.value);
521
606
  }
522
607
 
@@ -584,14 +669,83 @@ export class Interpreter {
584
669
  const func = node as FunctionNode;
585
670
  const funcName = (func.name as IdentifierNode).name;
586
671
 
672
+ // Get the function definition to check if it propagates empty
673
+ const functionDef = this.registry.getFunction(funcName);
674
+
587
675
  // Check if function is registered with an evaluator
588
676
  const functionEvaluator = this.functionEvaluators.get(funcName);
589
- if (functionEvaluator) {
590
- return await functionEvaluator(input, context, func.arguments, this.evaluate.bind(this));
677
+ if (!functionEvaluator) {
678
+ // No function found in registry
679
+ throw Errors.unknownFunction(funcName);
591
680
  }
681
+ // Helper: pick a matching signature based on argument count
682
+ const pickSignature = () => {
683
+ if (!functionDef?.signatures || functionDef.signatures.length === 0) {
684
+ return undefined as import('./types').FunctionSignature | undefined;
685
+ }
686
+ const argsCount = func.arguments.length;
687
+ for (const sig of functionDef.signatures) {
688
+ const total = sig.parameters.length;
689
+ const required = sig.parameters.filter(p => !p.optional).length;
690
+ if (argsCount >= required && argsCount <= total) {
691
+ return sig;
692
+ }
693
+ }
694
+ // Fallback to first signature
695
+ return functionDef.signatures[0];
696
+ };
697
+
698
+ const signature = pickSignature();
699
+
700
+ // Memoized evaluator to avoid duplicate evaluation of arguments
701
+ // Only memoize when both input and context references are identical to this call's input/context.
702
+ const originalInputRef = input;
703
+ const originalContextRef = context;
704
+ const cache = new WeakMap<ASTNode, Promise<EvaluationResult>>();
705
+ const memoEval = async (n: ASTNode, inVals: any[], ctx: RuntimeContext) => {
706
+ if (inVals === originalInputRef && ctx === originalContextRef) {
707
+ const cached = cache.get(n);
708
+ if (cached) {
709
+ return cached;
710
+ }
711
+ const promise = this.evaluate(n, inVals, ctx);
712
+ cache.set(n, promise);
713
+ return promise;
714
+ }
715
+ // Different input or context – do not reuse cached result
716
+ return this.evaluate(n, inVals, ctx);
717
+ };
718
+
719
+ // Handle empty propagation centrally (default: propagate)
720
+ if (functionDef && !functionDef.doesNotPropagateEmpty) {
721
+ // If input is empty, propagate empty immediately
722
+ if (input.length === 0) {
723
+ return { value: [], context };
724
+ }
592
725
 
593
- // No function found in registry
594
- throw Errors.unknownFunction(funcName);
726
+ // Evaluate non-expression, non-typeReference arguments once for emptiness
727
+ for (let i = 0; i < func.arguments.length; i++) {
728
+ const arg = func.arguments[i];
729
+ const param = signature?.parameters[i];
730
+ if (!arg || !param) {
731
+ continue;
732
+ }
733
+ // Skip expression or typeReference params (not evaluated here)
734
+ if (param.expression || param.typeReference) {
735
+ continue;
736
+ }
737
+ // Evaluate with memoization
738
+ const argResult = await memoEval(arg, input, context);
739
+ // If argument is empty and it's a required parameter, propagate empty
740
+ const isRequired = !param.optional;
741
+ if (isRequired && argResult.value.length === 0) {
742
+ return { value: [], context };
743
+ }
744
+ }
745
+ }
746
+
747
+ // Call the function evaluator with memoized evaluator
748
+ return await functionEvaluator(input, context, func.arguments, memoEval);
595
749
  }
596
750
 
597
751
  // Index evaluator
@@ -621,76 +775,14 @@ export class Interpreter {
621
775
  const test = node as MembershipTestNode;
622
776
  const exprResult = await this.evaluate(test.expression, input, context);
623
777
 
624
- // If expression evaluates to empty, return empty
625
- if (exprResult.value.length === 0) {
626
- return { value: [], context };
778
+ // Use the is-operator implementation for consistency
779
+ const isOperator = this.operationEvaluators.get('is');
780
+ if (isOperator) {
781
+ return isOperator(input, context, exprResult.value, [test.targetType]);
627
782
  }
628
783
 
629
- // If we have type information from analyzer (with ModelProvider), use it
630
- if (context.currentNode?.typeInfo?.modelContext) {
631
- const modelContext = context.currentNode.typeInfo.modelContext as any;
632
-
633
- // For union types, check if the target type is valid
634
- if (modelContext.isUnion && modelContext.choices) {
635
- const hasValidChoice = modelContext.choices.some((c: any) =>
636
- c.type === test.targetType || c.elementType === test.targetType
637
- );
638
-
639
- if (!hasValidChoice) {
640
- // Type system knows this will always be false
641
- return {
642
- value: exprResult.value.map(() => box(false, { type: 'Boolean', singleton: true })),
643
- context
644
- };
645
- }
646
- }
647
- }
648
-
649
- // Type checking with subtype support via ModelProvider
650
- const results = await Promise.all(exprResult.value.map(async boxedItem => {
651
- const item = unbox(boxedItem);
652
-
653
- // If we have a ModelProvider and typeInfo, use it for accurate subtype checking
654
- if (context.modelProvider && boxedItem.typeInfo) {
655
- const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, test.targetType as import('./types').TypeName);
656
- return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
657
- }
658
-
659
- // For FHIR resources without typeInfo, try to get it from modelProvider
660
- if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
661
- const typeInfo = await context.modelProvider.getType(item.resourceType);
662
- if (typeInfo) {
663
- const matchingType = context.modelProvider.ofType(typeInfo, test.targetType as import('./types').TypeName);
664
- return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
665
- }
666
- // Fall back to exact match
667
- return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
668
- }
669
-
670
- // Check for FHIR resource types (no ModelProvider available)
671
- if (item && typeof item === 'object' && 'resourceType' in item) {
672
- return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
673
- }
674
-
675
- // Check primitive types
676
- const isMatch = (() => {
677
- switch (test.targetType) {
678
- case 'String': return typeof item === 'string';
679
- case 'Boolean': return typeof item === 'boolean';
680
- case 'Integer': return Number.isInteger(item);
681
- case 'Decimal': return typeof item === 'number';
682
- case 'Date':
683
- case 'DateTime':
684
- case 'Time':
685
- // Simple check for date-like strings
686
- return typeof item === 'string' && !isNaN(Date.parse(item));
687
- default: return false;
688
- }
689
- })();
690
- return box(isMatch, { type: 'Boolean', singleton: true });
691
- }));
692
-
693
- return { value: results, context };
784
+ // Fallback - shouldn't reach here normally
785
+ return { value: [], context };
694
786
  }
695
787
 
696
788
  // Type cast (as operator)
@@ -698,76 +790,22 @@ export class Interpreter {
698
790
  const cast = node as TypeCastNode;
699
791
  const exprResult = await this.evaluate(cast.expression, input, context);
700
792
 
701
- // If we have type information from analyzer (with ModelProvider), use it
702
- if (context.currentNode?.typeInfo?.modelContext) {
703
- const modelContext = context.currentNode.typeInfo.modelContext as any;
704
-
705
- // For union types, check if the cast is valid
706
- if (modelContext.isUnion && modelContext.choices) {
707
- const validChoice = modelContext.choices.find((c: any) =>
708
- c.type === cast.targetType || c.elementType === cast.targetType
709
- );
710
-
711
- if (!validChoice) {
712
- // Invalid cast - return empty
713
- return { value: [], context };
714
- }
715
- }
793
+ // Use the as-operator implementation for consistency
794
+ const asOperator = this.operationEvaluators.get('as');
795
+ if (asOperator) {
796
+ return asOperator(input, context, exprResult.value, [cast.targetType]);
716
797
  }
717
798
 
718
- // Filter values that match the target type with subtype support
719
- const filtered = await Promise.all(exprResult.value.map(async boxedItem => {
720
- const item = unbox(boxedItem);
721
-
722
- // If we have a ModelProvider and typeInfo, use it for accurate subtype checking
723
- if (context.modelProvider && boxedItem.typeInfo) {
724
- const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, cast.targetType as import('./types').TypeName);
725
- return matchingType !== undefined;
726
- }
727
-
728
- // For FHIR resources without typeInfo, try to get it from modelProvider
729
- if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
730
- const typeInfo = await context.modelProvider.getType(item.resourceType);
731
- if (typeInfo) {
732
- const matchingType = context.modelProvider.ofType(typeInfo, cast.targetType as import('./types').TypeName);
733
- return matchingType !== undefined;
734
- }
735
- // Fall back to exact match
736
- return item.resourceType === cast.targetType;
737
- }
738
-
739
- // Check for FHIR resource types (no ModelProvider available)
740
- if (item && typeof item === 'object' && 'resourceType' in item) {
741
- return item.resourceType === cast.targetType;
742
- }
743
-
744
- // Check primitive types
745
- switch (cast.targetType) {
746
- case 'String': return typeof item === 'string';
747
- case 'Boolean': return typeof item === 'boolean';
748
- case 'Integer': return Number.isInteger(item);
749
- case 'Decimal': return typeof item === 'number';
750
- case 'Date':
751
- case 'DateTime':
752
- case 'Time':
753
- // Simple check for date-like strings
754
- return typeof item === 'string' && !isNaN(Date.parse(item));
755
- default: return false;
756
- }
757
- }));
758
-
759
- // Filter out the false results (filter returns boolean for each item)
760
- const actualFiltered = exprResult.value.filter((_, index) => filtered[index]);
761
-
762
- return { value: actualFiltered, context };
799
+ // Fallback implementation (shouldn't normally reach here)
800
+ return { value: [], context };
763
801
  }
764
802
 
765
803
  private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
766
804
  const quantity = node as QuantityNode;
767
- const quantityValue = createQuantity(quantity.value, quantity.unit, quantity.isCalendarUnit);
805
+ const quantityValue = createQuantity(quantity.value, quantity.unit);
768
806
  return {
769
807
  value: [box(quantityValue, { type: 'Quantity', singleton: true })],
770
808
  context
771
809
  };
772
810
  }
773
- }
811
+ }