@atomic-ehr/fhirpath 0.0.3 → 0.0.4

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 (67) hide show
  1. package/dist/index.browser.d.ts +22 -0
  2. package/dist/index.browser.js +15758 -0
  3. package/dist/index.browser.js.map +1 -0
  4. package/dist/index.node.d.ts +24 -0
  5. package/dist/{index.js → index.node.js} +5450 -3809
  6. package/dist/index.node.js.map +1 -0
  7. package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
  8. package/package.json +10 -5
  9. package/src/analyzer.ts +46 -9
  10. package/src/complex-types/quantity-value.ts +131 -9
  11. package/src/complex-types/temporal.ts +45 -6
  12. package/src/errors.ts +4 -0
  13. package/src/index.browser.ts +4 -0
  14. package/src/{index.ts → index.common.ts} +18 -14
  15. package/src/index.node.ts +4 -0
  16. package/src/interpreter/navigator.ts +12 -0
  17. package/src/interpreter/runtime-context.ts +60 -25
  18. package/src/interpreter.ts +118 -33
  19. package/src/lexer.ts +4 -1
  20. package/src/model-provider.browser.ts +35 -0
  21. package/src/{model-provider.ts → model-provider.common.ts} +29 -26
  22. package/src/model-provider.node.ts +41 -0
  23. package/src/operations/allTrue-function.ts +6 -10
  24. package/src/operations/and-operator.ts +2 -2
  25. package/src/operations/as-function.ts +41 -0
  26. package/src/operations/combine-operator.ts +17 -4
  27. package/src/operations/comparison.ts +73 -21
  28. package/src/operations/convertsToQuantity-function.ts +56 -7
  29. package/src/operations/decode-function.ts +114 -0
  30. package/src/operations/divide-operator.ts +3 -3
  31. package/src/operations/encode-function.ts +110 -0
  32. package/src/operations/escape-function.ts +114 -0
  33. package/src/operations/exp-function.ts +65 -0
  34. package/src/operations/extension-function.ts +88 -0
  35. package/src/operations/greater-operator.ts +5 -24
  36. package/src/operations/greater-or-equal-operator.ts +5 -24
  37. package/src/operations/hasValue-function.ts +84 -0
  38. package/src/operations/iif-function.ts +7 -1
  39. package/src/operations/implies-operator.ts +1 -0
  40. package/src/operations/index.ts +11 -0
  41. package/src/operations/is-function.ts +11 -0
  42. package/src/operations/is-operator.ts +187 -5
  43. package/src/operations/less-operator.ts +6 -24
  44. package/src/operations/less-or-equal-operator.ts +5 -24
  45. package/src/operations/less-than.ts +7 -12
  46. package/src/operations/ln-function.ts +62 -0
  47. package/src/operations/log-function.ts +113 -0
  48. package/src/operations/lowBoundary-function.ts +14 -0
  49. package/src/operations/minus-operator.ts +8 -1
  50. package/src/operations/mod-operator.ts +7 -1
  51. package/src/operations/not-function.ts +9 -2
  52. package/src/operations/ofType-function.ts +35 -0
  53. package/src/operations/plus-operator.ts +46 -3
  54. package/src/operations/precision-function.ts +146 -0
  55. package/src/operations/replace-function.ts +19 -19
  56. package/src/operations/replaceMatches-function.ts +5 -0
  57. package/src/operations/sort-function.ts +209 -0
  58. package/src/operations/take-function.ts +1 -1
  59. package/src/operations/toQuantity-function.ts +0 -1
  60. package/src/operations/toString-function.ts +76 -12
  61. package/src/operations/trace-function.ts +20 -3
  62. package/src/operations/unescape-function.ts +119 -0
  63. package/src/operations/where-function.ts +3 -1
  64. package/src/parser.ts +14 -2
  65. package/src/types.ts +7 -5
  66. package/src/utils/decimal.ts +76 -0
  67. package/dist/index.js.map +0 -1
@@ -1,36 +1,39 @@
1
1
  import { Parser } from './parser';
2
2
  import { Interpreter } from './interpreter';
3
3
  import { Analyzer } from './analyzer';
4
- import type { AnalysisResult } from './types';
5
- import { DiagnosticSeverity } from './types';
6
- import { FHIRPathError, Errors, ErrorCodes } from './errors';
4
+ import type { AnalysisResult, EvaluationResult, ModelProvider, TypeInfo } from './types';
5
+
6
+ declare const __VERSION__: string;
7
7
 
8
8
  export interface EvaluateOptions {
9
9
  input?: unknown;
10
10
  variables?: Record<string, unknown>;
11
- modelProvider?: import('./types').ModelProvider;
12
- inputType?: import('./types').TypeInfo;
11
+ modelProvider?: ModelProvider;
12
+ inputType?: TypeInfo;
13
+ includeMetadata?: boolean;
13
14
  }
14
15
 
15
- export async function evaluate(
16
+ export async function evaluate<T = any>(
16
17
  expression: string,
17
18
  options: EvaluateOptions = {}
18
- ): Promise<any[]> {
19
+ ): Promise<EvaluationResult<T>['value']> {
19
20
  const interpreter = new Interpreter(undefined, options.modelProvider);
20
21
  return interpreter.evaluateExpression(expression, {
21
22
  input: options.input,
22
23
  variables: options.variables,
23
24
  inputType: options.inputType,
24
25
  modelProvider: options.modelProvider,
26
+ includeMetadata: options.includeMetadata
25
27
  });
26
28
  }
27
29
 
30
+
28
31
  export async function analyze(
29
32
  expression: string,
30
33
  options: {
31
34
  variables?: Record<string, unknown>;
32
- modelProvider?: import('./types').ModelProvider;
33
- inputType?: import('./types').TypeInfo;
35
+ modelProvider?: ModelProvider;
36
+ inputType?: TypeInfo;
34
37
  errorRecovery?: boolean;
35
38
  } = {}
36
39
  ): Promise<AnalysisResult> {
@@ -43,6 +46,10 @@ export async function analyze(
43
46
  return analysisResult;
44
47
  }
45
48
 
49
+ export function getVersion(): string {
50
+ return __VERSION__;
51
+ }
52
+
46
53
  // Export key types and classes
47
54
  export { Parser } from './parser';
48
55
  export { Interpreter } from './interpreter';
@@ -59,13 +66,10 @@ export type {
59
66
  TypeName,
60
67
  ModelProvider as ModelTypeProvider,
61
68
  OperatorDefinition,
62
- FunctionDefinition
69
+ FunctionDefinition,
70
+ EvaluationResult
63
71
  } from './types';
64
72
 
65
- // Export FHIR ModelProvider
66
- export { FHIRModelProvider } from './model-provider';
67
- export type { FHIRModelContext, FHIRModelProviderConfig } from './model-provider';
68
-
69
73
  // Export inspect API
70
74
  export { inspect } from './inspect';
71
75
  export type { InspectOptions, InspectResult, ASTMetadata } from './inspect';
@@ -0,0 +1,4 @@
1
+ export * from './index.common';
2
+
3
+ export { FHIRModelProvider } from './model-provider.node.js';
4
+ export type { FHIRModelContext, FHIRModelProviderConfig } from './model-provider.node.js';
@@ -79,6 +79,18 @@ async function detectChoiceValues(
79
79
  } else {
80
80
  choiceType = { ...choiceType, singleton: !Array.isArray(value) };
81
81
  }
82
+
83
+ // For FHIR value[x] polymorphic properties, ensure FHIR namespace and lowercase name
84
+ if (choiceProp.startsWith('value') && modelProvider) {
85
+ // Convert type name to lowercase for FHIR primitive types
86
+ // valueString -> FHIR.string, valueInteger -> FHIR.integer, etc.
87
+ const lowerTypeName = choiceName.charAt(0).toLowerCase() + choiceName.slice(1);
88
+ choiceType = {
89
+ ...choiceType,
90
+ namespace: 'FHIR',
91
+ name: lowerTypeName
92
+ };
93
+ }
82
94
  if (Array.isArray(value)) {
83
95
  for (const v of value) {
84
96
  hits.push({ value: v, typeInfo: { ...choiceType, singleton: true }, primitiveElement });
@@ -33,6 +33,12 @@ export class RuntimeContextManager {
33
33
  context.variables['%resource'] = input;
34
34
  context.variables['%rootResource'] = input;
35
35
 
36
+ // Set FHIR-specific system variables (standard URLs for code systems)
37
+ context.variables['%sct'] = 'http://snomed.info/sct';
38
+ context.variables['%loinc'] = 'http://loinc.org';
39
+ context.variables['%ucum'] = 'http://unitsofmeasure.org';
40
+ context.variables['%vs-administrative-gender'] = 'http://hl7.org/fhir/ValueSet/administrative-gender';
41
+
36
42
  // Add any initial variables (with % prefix for user-defined)
37
43
  if (initialVariables) {
38
44
  for (const [key, value] of Object.entries(initialVariables)) {
@@ -91,7 +97,7 @@ export class RuntimeContextManager {
91
97
  static setVariable(context: RuntimeContext, name: string, value: any, allowRedefinition: boolean = false): RuntimeContext {
92
98
  // Ensure value is array for consistency (except for special variables like $index)
93
99
  const arrayValue = (name === '$index' || name === '$total') ? value :
94
- Array.isArray(value) ? value : [value];
100
+ Array.isArray(value) ? value : [value];
95
101
 
96
102
  // Determine variable key based on prefix
97
103
  let varKey = name;
@@ -100,12 +106,20 @@ export class RuntimeContextManager {
100
106
  varKey = `%${name}`;
101
107
  }
102
108
 
103
- // Check for system variables (with or without % prefix)
104
- const systemVariables = ['context', 'resource', 'rootResource', 'ucum', 'sct', 'loinc'];
105
- const baseVarName = varKey.startsWith('%') ? varKey.substring(1) : varKey;
106
- if (systemVariables.includes(baseVarName)) {
107
- // Throw error when trying to override system variables
108
- throw Errors.invalidOperation(`Cannot override system variable: ${baseVarName}`);
109
+ // Check for system variable override attempts
110
+ const systemVariables = new Set([
111
+ 'context', '%context',
112
+ 'resource', '%resource',
113
+ 'rootResource', '%rootResource',
114
+ 'sct', '%sct',
115
+ 'loinc', '%loinc',
116
+ 'ucum', '%ucum',
117
+ 'vs-administrative-gender', '%vs-administrative-gender'
118
+ ]);
119
+
120
+ if (!allowRedefinition && (systemVariables.has(name) || systemVariables.has(varKey))) {
121
+ // Cannot override system variables
122
+ throw Errors.systemVariableOverride(name);
109
123
  }
110
124
 
111
125
  // Check if variable already exists (unless redefinition is allowed)
@@ -149,12 +163,33 @@ export class RuntimeContextManager {
149
163
  if (name === 'rootResource' || name === '%rootResource') {
150
164
  return context.variables['%rootResource'];
151
165
  }
166
+ if (name === 'sct' || name === '%sct') {
167
+ return context.variables['%sct'];
168
+ }
169
+ if (name === 'loinc' || name === '%loinc') {
170
+ return context.variables['%loinc'];
171
+ }
172
+ if (name === 'ucum' || name === '%ucum') {
173
+ return context.variables['%ucum'];
174
+ }
175
+ if (name === 'vs-administrative-gender' || name === '%vs-administrative-gender' || name === '%`vs-administrative-gender`') {
176
+ return context.variables['%vs-administrative-gender'];
177
+ }
178
+
179
+ // Handle user-defined variables
180
+ // Strip backticks from environment variable syntax %`variable`
181
+ let cleanName = name;
182
+ if (name.startsWith('%`') && name.endsWith('`')) {
183
+ // Remove %` from start and ` from end
184
+ cleanName = '%' + name.slice(2, -1);
185
+ } else if (!name.startsWith('%')) {
186
+ // Add % prefix if not present
187
+ cleanName = '%' + name;
188
+ }
152
189
 
153
- // Handle user-defined variables (add % prefix if not present)
154
- const varKey = name.startsWith('%') ? name : `%${name}`;
155
190
  // Use 'in' operator to check prototype chain for inherited variables
156
- if (varKey in context.variables) {
157
- return context.variables[varKey];
191
+ if (cleanName in context.variables) {
192
+ return context.variables[cleanName];
158
193
  }
159
194
  return undefined;
160
195
  }
@@ -250,21 +285,21 @@ export class RuntimeContextManager {
250
285
  const values = Array.isArray(rawVal) ? rawVal : [rawVal];
251
286
  const maybeBoxed = modelProvider
252
287
  ? await Promise.all(
253
- values.map(async (v) => {
254
- if (
255
- v &&
256
- typeof v === 'object' &&
257
- 'resourceType' in (v as any) &&
258
- typeof (v as any).resourceType === 'string'
259
- ) {
260
- const ti = await modelProvider.getType((v as any).resourceType);
261
- return ti ? box(v, ti) : v;
262
- }
263
- return v;
264
- })
265
- )
288
+ values.map(async (v) => {
289
+ if (
290
+ v &&
291
+ typeof v === 'object' &&
292
+ 'resourceType' in (v as any) &&
293
+ typeof (v as any).resourceType === 'string'
294
+ ) {
295
+ const ti = await modelProvider.getType((v as any).resourceType);
296
+ return ti ? box(v, ti) : v;
297
+ }
298
+ return v;
299
+ })
300
+ )
266
301
  : values;
267
- context = RuntimeContextManager.setVariable(context, key, maybeBoxed);
302
+ context = RuntimeContextManager.setVariable(context, key, maybeBoxed, true);
268
303
  }
269
304
  }
270
305
 
@@ -10,7 +10,8 @@ import type {
10
10
  IndexNode,
11
11
  MembershipTestNode,
12
12
  TypeCastNode,
13
- QuantityNode
13
+ QuantityNode,
14
+ ModelProvider
14
15
  } from './types';
15
16
  import { NodeType } from './types';
16
17
  import { Registry } from './registry';
@@ -44,7 +45,7 @@ export class Interpreter {
44
45
  this.modelProvider = modelProvider;
45
46
  this.operationEvaluators = new Map();
46
47
  this.functionEvaluators = new Map();
47
-
48
+
48
49
  // Initialize node evaluators using object dispatch pattern
49
50
  this.nodeEvaluators = {
50
51
  [NodeType.Literal]: this.evaluateLiteral.bind(this),
@@ -136,10 +137,11 @@ export class Interpreter {
136
137
  input?: unknown;
137
138
  variables?: Record<string, unknown>;
138
139
  inputType?: TypeInfo;
139
- modelProvider?: import('./types').ModelProvider;
140
+ modelProvider?: ModelProvider;
140
141
  now?: Date;
142
+ includeMetadata?: boolean;
141
143
  } = {}
142
- ): Promise<any[]> {
144
+ ): Promise<EvaluationResult['value']> {
143
145
  // Analyze expression first (ensures type info and diagnostics)
144
146
  const analysis = await Analyzer.analyzeExpression(expression, {
145
147
  variables: options.variables,
@@ -168,13 +170,24 @@ export class Interpreter {
168
170
  // Unbox and format temporal outputs for API parity
169
171
  return result.value.map((boxedValue) => {
170
172
  const value = unbox(boxedValue);
173
+ let overridedValue: any = undefined;
171
174
  if (value && typeof value === 'object' && 'kind' in value) {
172
175
  if ((value as any).kind === 'FHIRDate' || (value as any).kind === 'FHIRDateTime') {
173
- return '@' + toTemporalString(value as any);
176
+ if (options.includeMetadata) {
177
+ overridedValue = '@' + toTemporalString(value as any);
178
+ } else {
179
+ return '@' + toTemporalString(value as any);
180
+ }
174
181
  } else if ((value as any).kind === 'FHIRTime') {
175
- return '@T' + toTemporalString(value as any);
182
+ if (options.includeMetadata) {
183
+ overridedValue = '@T' + toTemporalString(value as any);
184
+ } else {
185
+ return '@T' + toTemporalString(value as any);
186
+ }
176
187
  }
177
188
  }
189
+ // return options.includeMetadata ? box(overridedValue ?? value, boxedValue.typeInfo, boxedValue.primitiveElement) : value;
190
+ // return box(overridedValue ?? value, boxedValue.typeInfo, boxedValue.primitiveElement);
178
191
  return value;
179
192
  });
180
193
  }
@@ -201,10 +214,10 @@ export class Interpreter {
201
214
  // TemporalLiteral node evaluator
202
215
  private async evaluateTemporalLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
203
216
  const temporal = node as import('./types').TemporalLiteralNode;
204
-
217
+
205
218
  // The value is already parsed in the parser
206
219
  let typeInfo: import('./types').TypeInfo;
207
-
220
+
208
221
  if (temporal.valueType === 'date') {
209
222
  typeInfo = { type: 'Date', singleton: true };
210
223
  } else if (temporal.valueType === 'datetime') {
@@ -212,7 +225,7 @@ export class Interpreter {
212
225
  } else {
213
226
  typeInfo = { type: 'Time', singleton: true };
214
227
  }
215
-
228
+
216
229
  return {
217
230
  value: [box(temporal.value, typeInfo)],
218
231
  context
@@ -222,18 +235,18 @@ export class Interpreter {
222
235
  // Literal node evaluator
223
236
  private async evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
224
237
  const literal = node as LiteralNode;
225
-
238
+
226
239
  // Box the literal value with appropriate type info
227
240
  let typeInfo: import('./types').TypeInfo | undefined;
228
241
  let value: any = literal.value;
229
-
242
+
230
243
  // Handle temporal literals (backwards compatibility - should not reach here with new parser)
231
244
  if (literal.valueType === 'date' || literal.valueType === 'datetime' || literal.valueType === 'time') {
232
245
  // Import temporal parsing function
233
246
  const { parseTemporalLiteral } = await import('./complex-types/temporal');
234
247
  // Parse the temporal literal (add @ back since it was stripped by parser)
235
248
  const temporalValue = parseTemporalLiteral('@' + literal.value);
236
-
249
+
237
250
  // Set appropriate type info
238
251
  if (literal.valueType === 'date') {
239
252
  typeInfo = { type: 'Date', singleton: true };
@@ -242,18 +255,20 @@ export class Interpreter {
242
255
  } else if (literal.valueType === 'time') {
243
256
  typeInfo = { type: 'Time', singleton: true };
244
257
  }
245
-
258
+
246
259
  value = temporalValue;
247
260
  } else if (typeof value === 'string') {
248
261
  typeInfo = { type: 'String', singleton: true };
249
262
  } else if (typeof value === 'number') {
250
- typeInfo = Number.isInteger(value) ?
251
- { type: 'Integer', singleton: true } :
252
- { type: 'Decimal', singleton: true };
263
+ // Use the valueType from the literal node to determine if it's integer or decimal
264
+ // This preserves the distinction between 1.0 (decimal) and 1 (integer)
265
+ typeInfo = literal.valueType === 'decimal' ?
266
+ { type: 'Decimal', singleton: true } :
267
+ { type: 'Integer', singleton: true };
253
268
  } else if (typeof value === 'boolean') {
254
269
  typeInfo = { type: 'Boolean', singleton: true };
255
270
  }
256
-
271
+
257
272
  return {
258
273
  value: [box(value, typeInfo)],
259
274
  context
@@ -262,7 +277,7 @@ export class Interpreter {
262
277
 
263
278
  // Helper: Handle extension elements
264
279
  private handleExtension(
265
- boxedItem: FHIRPathValue,
280
+ boxedItem: FHIRPathValue,
266
281
  nodeTypeInfo?: TypeInfo
267
282
  ): FHIRPathValue[] {
268
283
  const results: FHIRPathValue[] = [];
@@ -326,15 +341,31 @@ export class Interpreter {
326
341
  item: object,
327
342
  name: string,
328
343
  nodeTypeInfo: TypeInfo | undefined,
329
- context: RuntimeContext
344
+ context: RuntimeContext,
345
+ parentTypeInfo?: TypeInfo
330
346
  ): Promise<FHIRPathValue[]> {
331
347
  const results: FHIRPathValue[] = [];
332
348
  if (name in (item as any)) {
333
349
  const value = (item as any)[name];
334
350
  const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, name);
335
351
 
352
+ // Determine if this is a FHIR primitive - if parent is a FHIR resource and value is primitive
353
+ const isFHIRPrimitive = parentTypeInfo &&
354
+ parentTypeInfo.type &&
355
+ parentTypeInfo.type !== 'Any' &&
356
+ !parentTypeInfo.type.startsWith('System.') &&
357
+ (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number');
358
+
336
359
  if (Array.isArray(value)) {
337
- const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
360
+ let elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
361
+
362
+ // For FHIR primitives, use FHIR namespace
363
+ if (isFHIRPrimitive && elementTypeInfo) {
364
+ if (elementTypeInfo.type === 'Boolean') {
365
+ elementTypeInfo = { ...elementTypeInfo, type: 'boolean' as any };
366
+ }
367
+ }
368
+
338
369
  for (const v of value) {
339
370
  if (
340
371
  v && typeof v === 'object' && 'resourceType' in (v as any) && typeof (v as any).resourceType === 'string'
@@ -355,8 +386,16 @@ export class Interpreter {
355
386
  const boxed = await reboxResource(value, true, context.modelProvider);
356
387
  results.push(boxed);
357
388
  } else {
358
- const val = await maybeParseTemporal(value, nodeTypeInfo, context.modelProvider);
359
- results.push(box(val, nodeTypeInfo, primitiveElement));
389
+ // For FHIR primitives, use FHIR namespace
390
+ let typeInfo = nodeTypeInfo;
391
+ if (isFHIRPrimitive && typeInfo) {
392
+ if (typeInfo.type === 'Boolean') {
393
+ typeInfo = { ...typeInfo, type: 'boolean' as any };
394
+ }
395
+ }
396
+
397
+ const val = await maybeParseTemporal(value, typeInfo, context.modelProvider);
398
+ results.push(box(val, typeInfo, primitiveElement));
360
399
  }
361
400
  }
362
401
  }
@@ -393,7 +432,7 @@ export class Interpreter {
393
432
  results.push(...unionResults);
394
433
 
395
434
  // 4. Handle standard property access
396
- const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context);
435
+ const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context, boxedItem.typeInfo);
397
436
  results.push(...propertyResults);
398
437
  }
399
438
  }
@@ -431,9 +470,29 @@ export class Interpreter {
431
470
 
432
471
  // Special handling for dot operator (sequential pipeline)
433
472
  if (operator === '.') {
473
+ // Check if this is actually a namespaced type in an 'is' expression
474
+ // Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
475
+ if (binary.left.type === NodeType.MembershipTest && binary.right.type === NodeType.Identifier) {
476
+ const membershipTest = binary.left as MembershipTestNode;
477
+ const rightIdent = binary.right as IdentifierNode;
478
+
479
+ // Extract the expression from the membership test
480
+ const expr = membershipTest.expression;
481
+ const typeName = `${membershipTest.targetType}.${rightIdent.name}`;
482
+
483
+ // Evaluate the expression
484
+ const exprResult = await this.evaluate(expr, input, context);
485
+
486
+ // Now apply the is operator with the full type name
487
+ const evaluator = this.operationEvaluators.get('is');
488
+ if (evaluator) {
489
+ return await evaluator(input, context, exprResult.value, [typeName]);
490
+ }
491
+ }
492
+
434
493
  // Evaluate left with current input/context
435
494
  const leftResult = await this.evaluate(binary.left, input, context);
436
-
495
+
437
496
  // Use left's output as right's input, and left's context flows to right
438
497
  return await this.evaluate(binary.right, leftResult.value, leftResult.context);
439
498
  }
@@ -447,7 +506,7 @@ export class Interpreter {
447
506
  this.evaluate(binary.left, input, context),
448
507
  this.evaluate(binary.right, input, context)
449
508
  ]);
450
-
509
+
451
510
  // Merge the results
452
511
  const unionEvaluator = this.operationEvaluators.get('|');
453
512
  if (unionEvaluator) {
@@ -457,6 +516,32 @@ export class Interpreter {
457
516
  throw Errors.noEvaluatorFound('binary operator', '|');
458
517
  }
459
518
 
519
+ // Special handling for 'is' and 'as' operators - right side is a type identifier, not an expression
520
+ if (operator === 'is' || operator === 'as') {
521
+ const leftResult = await this.evaluate(binary.left, input, context);
522
+
523
+ // Extract type name from right side WITHOUT evaluating it
524
+ let typeName: string;
525
+ if (binary.right.type === NodeType.Identifier) {
526
+ typeName = (binary.right as any).name;
527
+ } else if (binary.right.type === NodeType.Binary && (binary.right as any).operator === '.') {
528
+ // Handle namespaced types like System.Boolean or FHIR.Patient
529
+ const rightBinary = binary.right as any;
530
+ if (rightBinary.left.type === NodeType.Identifier && rightBinary.right.type === NodeType.Identifier) {
531
+ typeName = `${rightBinary.left.name}.${rightBinary.right.name}`;
532
+ } else {
533
+ throw new Error('is operator requires a type name as right operand');
534
+ }
535
+ } else {
536
+ throw new Error('is operator requires a type name as right operand');
537
+ }
538
+
539
+ const evaluator = this.operationEvaluators.get('is');
540
+ if (evaluator) {
541
+ return await evaluator(input, context, leftResult.value, [typeName]);
542
+ }
543
+ }
544
+
460
545
  // Get operation evaluator
461
546
  const evaluator = this.operationEvaluators.get(operator);
462
547
  if (evaluator) {
@@ -541,7 +626,7 @@ export class Interpreter {
541
626
  private async evaluateUnary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
542
627
  const unary = node as UnaryNode;
543
628
  const operator = unary.operator;
544
-
629
+
545
630
  const operandResult = await this.evaluate(unary.operand, input, context);
546
631
 
547
632
  // Check for unary operation evaluators
@@ -566,7 +651,7 @@ export class Interpreter {
566
651
  const name = variable.name;
567
652
 
568
653
  const value = RuntimeContextManager.getVariable(context, name);
569
-
654
+
570
655
  if (value !== undefined) {
571
656
  // Ensure value is always an array
572
657
  const arrayValue = Array.isArray(value) ? value : [value];
@@ -599,7 +684,7 @@ export class Interpreter {
599
684
 
600
685
  // Get the function definition to check if it propagates empty
601
686
  const functionDef = this.registry.getFunction(funcName);
602
-
687
+
603
688
  // Check if function is registered with an evaluator
604
689
  const functionEvaluator = this.functionEvaluators.get(funcName);
605
690
  if (!functionEvaluator) {
@@ -702,13 +787,13 @@ export class Interpreter {
702
787
  private async evaluateMembershipTest(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
703
788
  const test = node as MembershipTestNode;
704
789
  const exprResult = await this.evaluate(test.expression, input, context);
705
-
790
+
706
791
  // Use the is-operator implementation for consistency
707
792
  const isOperator = this.operationEvaluators.get('is');
708
793
  if (isOperator) {
709
794
  return isOperator(input, context, exprResult.value, [test.targetType]);
710
795
  }
711
-
796
+
712
797
  // Fallback - shouldn't reach here normally
713
798
  return { value: [], context };
714
799
  }
@@ -717,17 +802,17 @@ export class Interpreter {
717
802
  private async evaluateTypeCast(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
718
803
  const cast = node as TypeCastNode;
719
804
  const exprResult = await this.evaluate(cast.expression, input, context);
720
-
805
+
721
806
  // Use the as-operator implementation for consistency
722
807
  const asOperator = this.operationEvaluators.get('as');
723
808
  if (asOperator) {
724
809
  return asOperator(input, context, exprResult.value, [cast.targetType]);
725
810
  }
726
-
811
+
727
812
  // Fallback implementation (shouldn't normally reach here)
728
813
  return { value: [], context };
729
814
  }
730
-
815
+
731
816
  private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
732
817
  const quantity = node as QuantityNode;
733
818
  const quantityValue = createQuantity(quantity.value, quantity.unit);
package/src/lexer.ts CHANGED
@@ -674,10 +674,13 @@ export class Lexer {
674
674
  if (this.current() === '*' && this.peek() === '/') {
675
675
  this.advance(); // Skip *
676
676
  this.advance(); // Skip /
677
- break;
677
+ return;
678
678
  }
679
679
  this.advance();
680
680
  }
681
+
682
+ // If we reached the end without finding */, it's an error
683
+ throw this.error('Unclosed multi-line comment');
681
684
  }
682
685
 
683
686
  private advance(): void {
@@ -0,0 +1,35 @@
1
+ export * from './model-provider.common'
2
+ import { FHIRModelProviderBase, type Resource } from './model-provider.common';
3
+
4
+ export type Resolver = (canonicalUrl: string) => Promise<Resource | null>;
5
+ export type Searcher = (kind: 'primitive-type' | 'complex-type' | 'resource') => Promise<Resource[]>
6
+
7
+ export type Options = {
8
+ resolve: Resolver,
9
+ search: Searcher
10
+ }
11
+
12
+ export class FHIRModelProvider extends FHIRModelProviderBase {
13
+ private _resolve: Resolver;
14
+ private _search: Searcher;
15
+
16
+
17
+ override async resolve(canonicalUrl: string): Promise<Resource | null> {
18
+ return await this._resolve(canonicalUrl);
19
+ }
20
+
21
+ override async search(params: { kind: 'primitive-type' | 'complex-type' | 'resource' }): Promise<Resource[]> {
22
+ return await this._search(params.kind);
23
+ }
24
+
25
+ reconfigure(options: Options) {
26
+ this._resolve = options.resolve;
27
+ this._search = options.search;
28
+ }
29
+
30
+ constructor(options: Options) {
31
+ super();
32
+ this._resolve = options.resolve;
33
+ this._search = options.search;
34
+ }
35
+ }