@atomic-ehr/fhirpath 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
@@ -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,15 +125,126 @@ 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
250
  typeInfo = Number.isInteger(value) ?
@@ -275,207 +255,173 @@ export class Interpreter {
275
255
  }
276
256
 
277
257
  return {
278
- value: [box(literal.value, typeInfo)],
258
+ value: [box(value, typeInfo)],
279
259
  context
280
260
  };
281
261
  }
282
262
 
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;
287
-
288
- // Navigate property on each boxed item in input
263
+ // Helper: Handle extension elements
264
+ private handleExtension(
265
+ boxedItem: FHIRPathValue,
266
+ nodeTypeInfo?: TypeInfo
267
+ ): FHIRPathValue[] {
289
268
  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 }));
302
- }
303
- continue;
269
+ if (boxedItem.primitiveElement?.extension) {
270
+ for (const ext of boxedItem.primitiveElement.extension) {
271
+ results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
304
272
  }
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;
310
-
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
- }
354
- }
355
- }
356
-
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
- }
386
- }
387
- }
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
-
273
+ }
274
+ return results;
275
+ }
276
+
277
+ // Helper: Handle FHIR choice types (e.g., value[x])
278
+ private async handleChoiceTypes(
279
+ item: object,
280
+ name: string,
281
+ context: RuntimeContext
282
+ ): Promise<FHIRPathValue[]> {
283
+ const results: FHIRPathValue[] = [];
284
+ const choiceHits = await detectChoiceValues(item as Record<string, unknown>, name, context.modelProvider);
285
+ for (const hit of choiceHits) {
286
+ results.push(box(hit.value, hit.typeInfo, hit.primitiveElement));
287
+ }
288
+ return results;
289
+ }
290
+
291
+ // Helper: Handle union type choices
292
+ private handleUnionChoices(
293
+ item: object,
294
+ nodeTypeInfo?: TypeInfo
295
+ ): FHIRPathValue[] {
296
+ const results: FHIRPathValue[] = [];
297
+ if (
298
+ nodeTypeInfo?.modelContext &&
299
+ typeof nodeTypeInfo.modelContext === 'object' &&
300
+ 'isUnion' in nodeTypeInfo.modelContext &&
301
+ (nodeTypeInfo.modelContext as any).isUnion &&
302
+ 'choices' in nodeTypeInfo.modelContext &&
303
+ Array.isArray((nodeTypeInfo.modelContext as any).choices)
304
+ ) {
305
+ for (const choice of (nodeTypeInfo.modelContext as any).choices) {
306
+ const choiceName = choice.choiceName;
307
+ if (choiceName && choiceName in (item as any)) {
308
+ const value = (item as any)[choiceName];
309
+ const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, choiceName);
310
+ const choiceTypeInfo = { type: choice.type, singleton: !Array.isArray(value), modelContext: choice } as any;
395
311
  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
312
  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
- }
313
+ results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
424
314
  }
425
315
  } 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));
450
- }
316
+ results.push(box(value, choiceTypeInfo, primitiveElement));
451
317
  }
452
318
  }
453
319
  }
454
320
  }
321
+ return results;
322
+ }
455
323
 
456
- return {
457
- value: results,
458
- context
459
- };
324
+ // Helper: Handle standard property access
325
+ private async handleStandardProperty(
326
+ item: object,
327
+ name: string,
328
+ nodeTypeInfo: TypeInfo | undefined,
329
+ context: RuntimeContext
330
+ ): Promise<FHIRPathValue[]> {
331
+ const results: FHIRPathValue[] = [];
332
+ if (name in (item as any)) {
333
+ const value = (item as any)[name];
334
+ const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, name);
335
+
336
+ if (Array.isArray(value)) {
337
+ const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
338
+ for (const v of value) {
339
+ if (
340
+ v && typeof v === 'object' && 'resourceType' in (v as any) && typeof (v as any).resourceType === 'string'
341
+ ) {
342
+ // Always re-box FHIR resources to get proper type information from ModelProvider
343
+ const boxed = await reboxResource(v, true, context.modelProvider);
344
+ results.push(boxed);
345
+ } else {
346
+ const val = await maybeParseTemporal(v, elementTypeInfo, context.modelProvider);
347
+ results.push(box(val, elementTypeInfo, primitiveElement));
348
+ }
349
+ }
350
+ } else if (value !== null && value !== undefined) {
351
+ if (
352
+ value && typeof value === 'object' && 'resourceType' in (value as any) && typeof (value as any).resourceType === 'string'
353
+ ) {
354
+ // Always re-box FHIR resources to get proper type information from ModelProvider
355
+ const boxed = await reboxResource(value, true, context.modelProvider);
356
+ results.push(boxed);
357
+ } else {
358
+ const val = await maybeParseTemporal(value, nodeTypeInfo, context.modelProvider);
359
+ results.push(box(val, nodeTypeInfo, primitiveElement));
360
+ }
361
+ }
362
+ }
363
+ return results;
460
364
  }
461
365
 
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;
366
+ // Identifier node evaluator
367
+ private async evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
368
+ const identifier = node as IdentifierNode;
369
+ const name = identifier.name;
370
+ const nodeTypeInfo = node.typeInfo;
371
+ const results: FHIRPathValue[] = [];
466
372
 
467
- // First try as type filter
468
- const filtered = input.filter(boxedItem => {
373
+ for (const boxedItem of input) {
469
374
  const item = unbox(boxedItem);
470
- return item && typeof item === 'object' && item.resourceType === name;
471
- });
472
375
 
473
- if (filtered.length > 0) {
474
- return { value: filtered, context };
376
+ // 1. Handle extension special case
377
+ if (name === 'extension') {
378
+ results.push(...this.handleExtension(boxedItem, nodeTypeInfo));
379
+ continue;
380
+ }
381
+
382
+ // Process only objects
383
+ if (item && typeof item === 'object') {
384
+ // 2. Handle FHIR choice types (e.g., value[x])
385
+ const choiceResults = await this.handleChoiceTypes(item, name, context);
386
+ if (choiceResults.length > 0) {
387
+ results.push(...choiceResults);
388
+ continue;
389
+ }
390
+
391
+ // 3. Handle union type choices
392
+ const unionResults = this.handleUnionChoices(item, nodeTypeInfo);
393
+ results.push(...unionResults);
394
+
395
+ // 4. Handle standard property access
396
+ const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context);
397
+ results.push(...propertyResults);
398
+ }
475
399
  }
476
400
 
477
- // Otherwise treat as identifier
478
- return await this.evaluateIdentifier(node, input, context);
401
+ // If no properties matched, try type-filter fallback on resources
402
+ if (results.length === 0) {
403
+ const filtered: FHIRPathValue[] = [];
404
+ for (const boxedItem of input) {
405
+ const item = unbox(boxedItem);
406
+ if (item && typeof item === 'object' && (item as any).resourceType === name) {
407
+ if (context.modelProvider) {
408
+ const typeInfo = await context.modelProvider.getType(name);
409
+ if (typeInfo) {
410
+ filtered.push(box(item, { ...typeInfo, singleton: true }));
411
+ } else {
412
+ filtered.push(boxedItem);
413
+ }
414
+ } else {
415
+ filtered.push(boxedItem);
416
+ }
417
+ }
418
+ }
419
+ if (filtered.length > 0) {
420
+ return { value: filtered, context };
421
+ }
422
+ }
423
+
424
+ return { value: results, context };
479
425
  }
480
426
 
481
427
  // Binary operator evaluator
@@ -487,6 +433,7 @@ export class Interpreter {
487
433
  if (operator === '.') {
488
434
  // Evaluate left with current input/context
489
435
  const leftResult = await this.evaluate(binary.left, input, context);
436
+
490
437
  // Use left's output as right's input, and left's context flows to right
491
438
  return await this.evaluate(binary.right, leftResult.value, leftResult.context);
492
439
  }
@@ -495,28 +442,94 @@ export class Interpreter {
495
442
  if (operator === '|') {
496
443
  // Each side of union should have its own variable scope
497
444
  // 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
445
+ // Evaluate both sides in parallel since both use the same input/context
446
+ const [leftResult, rightResult] = await Promise.all([
447
+ this.evaluate(binary.left, input, context),
448
+ this.evaluate(binary.right, input, context)
449
+ ]);
500
450
 
501
451
  // Merge the results
502
- const unionEvaluator = this.operationEvaluators.get('union');
452
+ const unionEvaluator = this.operationEvaluators.get('|');
503
453
  if (unionEvaluator) {
504
454
  return await unionEvaluator(input, context, leftResult.value, rightResult.value);
505
455
  }
506
-
507
- // Fallback if union evaluator not found
508
- return {
509
- value: [...leftResult.value, ...rightResult.value],
510
- context // Original context preserved
511
- };
456
+ // If union evaluator not found, surface a clear error
457
+ throw Errors.noEvaluatorFound('binary operator', '|');
512
458
  }
513
459
 
514
460
  // Get operation evaluator
515
461
  const evaluator = this.operationEvaluators.get(operator);
516
462
  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);
463
+ // Short-circuit for logical operators when possible
464
+ if (operator === 'and' || operator === 'or' || operator === 'implies') {
465
+ const leftResult = await this.evaluate(binary.left, input, context);
466
+ const kind = this.getBooleanKind(leftResult.value);
467
+
468
+ if (operator === 'and') {
469
+ // false and _ -> false (short-circuit)
470
+ if (kind === 'false') {
471
+ return { value: [this.boxBoolean(false)], context };
472
+ }
473
+ // true and y -> y; empty and false -> false handled by evaluator; need right
474
+ const rightResult = await this.evaluate(binary.right, input, context);
475
+ const operatorDef = this.registry.getOperatorDefinition(operator);
476
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
477
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
478
+ return { value: [], context };
479
+ }
480
+ }
481
+ return await evaluator(input, context, leftResult.value, rightResult.value);
482
+ }
483
+
484
+ if (operator === 'or') {
485
+ // true or _ -> true (short-circuit)
486
+ if (kind === 'true') {
487
+ return { value: [this.boxBoolean(true)], context };
488
+ }
489
+ // false or y -> y; empty or true -> true handled by evaluator; need right
490
+ const rightResult = await this.evaluate(binary.right, input, context);
491
+ const operatorDef = this.registry.getOperatorDefinition(operator);
492
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
493
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
494
+ return { value: [], context };
495
+ }
496
+ }
497
+ return await evaluator(input, context, leftResult.value, rightResult.value);
498
+ }
499
+
500
+ if (operator === 'implies') {
501
+ // false implies _ -> true (short-circuit)
502
+ if (kind === 'false') {
503
+ return { value: [this.boxBoolean(true)], context };
504
+ }
505
+ // true implies y -> y; empty implies y -> true if y true else empty; need right
506
+ const rightResult = await this.evaluate(binary.right, input, context);
507
+ const operatorDef = this.registry.getOperatorDefinition(operator);
508
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
509
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
510
+ return { value: [], context };
511
+ }
512
+ }
513
+ return await evaluator(input, context, leftResult.value, rightResult.value);
514
+ }
515
+ }
516
+
517
+ // Default path: evaluate both operands in parallel
518
+ const [leftResult, rightResult] = await Promise.all([
519
+ this.evaluate(binary.left, input, context),
520
+ this.evaluate(binary.right, input, context)
521
+ ]);
522
+
523
+ // Handle empty propagation for operators
524
+ // Rely exclusively on registry metadata
525
+ const operatorDef = this.registry.getOperatorDefinition(operator);
526
+ if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
527
+ // Check if either operand is empty
528
+ if (leftResult.value.length === 0 || rightResult.value.length === 0) {
529
+ return { value: [], context };
530
+ }
531
+ }
532
+
520
533
  return await evaluator(input, context, leftResult.value, rightResult.value);
521
534
  }
522
535
 
@@ -584,14 +597,83 @@ export class Interpreter {
584
597
  const func = node as FunctionNode;
585
598
  const funcName = (func.name as IdentifierNode).name;
586
599
 
600
+ // Get the function definition to check if it propagates empty
601
+ const functionDef = this.registry.getFunction(funcName);
602
+
587
603
  // Check if function is registered with an evaluator
588
604
  const functionEvaluator = this.functionEvaluators.get(funcName);
589
- if (functionEvaluator) {
590
- return await functionEvaluator(input, context, func.arguments, this.evaluate.bind(this));
605
+ if (!functionEvaluator) {
606
+ // No function found in registry
607
+ throw Errors.unknownFunction(funcName);
591
608
  }
609
+ // Helper: pick a matching signature based on argument count
610
+ const pickSignature = () => {
611
+ if (!functionDef?.signatures || functionDef.signatures.length === 0) {
612
+ return undefined as import('./types').FunctionSignature | undefined;
613
+ }
614
+ const argsCount = func.arguments.length;
615
+ for (const sig of functionDef.signatures) {
616
+ const total = sig.parameters.length;
617
+ const required = sig.parameters.filter(p => !p.optional).length;
618
+ if (argsCount >= required && argsCount <= total) {
619
+ return sig;
620
+ }
621
+ }
622
+ // Fallback to first signature
623
+ return functionDef.signatures[0];
624
+ };
592
625
 
593
- // No function found in registry
594
- throw Errors.unknownFunction(funcName);
626
+ const signature = pickSignature();
627
+
628
+ // Memoized evaluator to avoid duplicate evaluation of arguments
629
+ // Only memoize when both input and context references are identical to this call's input/context.
630
+ const originalInputRef = input;
631
+ const originalContextRef = context;
632
+ const cache = new WeakMap<ASTNode, Promise<EvaluationResult>>();
633
+ const memoEval = async (n: ASTNode, inVals: any[], ctx: RuntimeContext) => {
634
+ if (inVals === originalInputRef && ctx === originalContextRef) {
635
+ const cached = cache.get(n);
636
+ if (cached) {
637
+ return cached;
638
+ }
639
+ const promise = this.evaluate(n, inVals, ctx);
640
+ cache.set(n, promise);
641
+ return promise;
642
+ }
643
+ // Different input or context – do not reuse cached result
644
+ return this.evaluate(n, inVals, ctx);
645
+ };
646
+
647
+ // Handle empty propagation centrally (default: propagate)
648
+ if (functionDef && !functionDef.doesNotPropagateEmpty) {
649
+ // If input is empty, propagate empty immediately
650
+ if (input.length === 0) {
651
+ return { value: [], context };
652
+ }
653
+
654
+ // Evaluate non-expression, non-typeReference arguments once for emptiness
655
+ for (let i = 0; i < func.arguments.length; i++) {
656
+ const arg = func.arguments[i];
657
+ const param = signature?.parameters[i];
658
+ if (!arg || !param) {
659
+ continue;
660
+ }
661
+ // Skip expression or typeReference params (not evaluated here)
662
+ if (param.expression || param.typeReference) {
663
+ continue;
664
+ }
665
+ // Evaluate with memoization
666
+ const argResult = await memoEval(arg, input, context);
667
+ // If argument is empty and it's a required parameter, propagate empty
668
+ const isRequired = !param.optional;
669
+ if (isRequired && argResult.value.length === 0) {
670
+ return { value: [], context };
671
+ }
672
+ }
673
+ }
674
+
675
+ // Call the function evaluator with memoized evaluator
676
+ return await functionEvaluator(input, context, func.arguments, memoEval);
595
677
  }
596
678
 
597
679
  // Index evaluator
@@ -621,76 +703,14 @@ export class Interpreter {
621
703
  const test = node as MembershipTestNode;
622
704
  const exprResult = await this.evaluate(test.expression, input, context);
623
705
 
624
- // If expression evaluates to empty, return empty
625
- if (exprResult.value.length === 0) {
626
- return { value: [], context };
627
- }
628
-
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
- }
706
+ // Use the is-operator implementation for consistency
707
+ const isOperator = this.operationEvaluators.get('is');
708
+ if (isOperator) {
709
+ return isOperator(input, context, exprResult.value, [test.targetType]);
647
710
  }
648
711
 
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 };
712
+ // Fallback - shouldn't reach here normally
713
+ return { value: [], context };
694
714
  }
695
715
 
696
716
  // Type cast (as operator)
@@ -698,76 +718,22 @@ export class Interpreter {
698
718
  const cast = node as TypeCastNode;
699
719
  const exprResult = await this.evaluate(cast.expression, input, context);
700
720
 
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
- }
721
+ // Use the as-operator implementation for consistency
722
+ const asOperator = this.operationEvaluators.get('as');
723
+ if (asOperator) {
724
+ return asOperator(input, context, exprResult.value, [cast.targetType]);
716
725
  }
717
726
 
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 };
727
+ // Fallback implementation (shouldn't normally reach here)
728
+ return { value: [], context };
763
729
  }
764
730
 
765
731
  private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
766
732
  const quantity = node as QuantityNode;
767
- const quantityValue = createQuantity(quantity.value, quantity.unit, quantity.isCalendarUnit);
733
+ const quantityValue = createQuantity(quantity.value, quantity.unit);
768
734
  return {
769
735
  value: [box(quantityValue, { type: 'Quantity', singleton: true })],
770
736
  context
771
737
  };
772
738
  }
773
- }
739
+ }