@atomic-ehr/fhirpath 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
package/src/errors.ts CHANGED
@@ -70,6 +70,10 @@ export const Errors = {
70
70
  return new FHIRPathError(ErrorCodes.VARIABLE_NOT_DEFINED, `Variable '${name}' is not defined in the current scope`, location);
71
71
  },
72
72
 
73
+ variableAlreadyDefined(name: string, location?: Range): FHIRPathError {
74
+ return new FHIRPathError(ErrorCodes.VARIABLE_ALREADY_DEFINED, `Variable '${name}' already defined in current scope`, location);
75
+ },
76
+
73
77
  // Arity errors (2000-2999)
74
78
  wrongArgumentCount(funcName: string, expected: number, actual: number, location?: Range): FHIRPathError {
75
79
  return new FHIRPathError(ErrorCodes.WRONG_ARGUMENT_COUNT, `${funcName} expects ${expected} arguments, got ${actual}`, location);
@@ -192,6 +196,18 @@ export const Errors = {
192
196
 
193
197
  invalidNumericOperation(operation: string, paramName: string, expectedType: string, location?: Range): FHIRPathError {
194
198
  return new FHIRPathError(ErrorCodes.INVALID_NUMERIC_OPERATION, `${operation} ${paramName} must be ${expectedType}`, location);
199
+ },
200
+
201
+ invalidTemporalUnit(temporalType: string, unit: string, location?: Range): FHIRPathError {
202
+ return new FHIRPathError(ErrorCodes.INVALID_TEMPORAL_UNIT, `Cannot use variable-duration unit '${unit}' with ${temporalType} - use calendar duration keywords instead`, location);
203
+ },
204
+
205
+ unsupportedTemporalUnitForType(temporalType: string, unit: string, location?: Range): FHIRPathError {
206
+ return new FHIRPathError(
207
+ ErrorCodes.UNSUPPORTED_TEMPORAL_UNIT_FOR_TYPE,
208
+ `Unit '${unit}' is not allowed for ${temporalType} arithmetic; allowed units: years, months, weeks, days`,
209
+ location
210
+ );
195
211
  }
196
212
  };
197
213
 
@@ -216,7 +232,7 @@ export enum ErrorCodes {
216
232
 
217
233
  // Type errors (3000-3999)
218
234
  // FP3001 - removed (unified with FP3006)
219
- OPERATOR_TYPE_MISMATCH = 'FP3002',
235
+ OPERATOR_TYPE_MISMATCH = 'FP3006',
220
236
  ARGUMENT_TYPE_MISMATCH = 'FP3003',
221
237
  CONVERSION_FAILED = 'FP3004',
222
238
  INVALID_VALUE_TYPE = 'FP3005',
@@ -243,5 +259,11 @@ export enum ErrorCodes {
243
259
  INVALID_OPERATION = 'FP6005',
244
260
  INVALID_PRECISION = 'FP6006',
245
261
  INVALID_STRING_OPERATION = 'FP6007',
246
- INVALID_NUMERIC_OPERATION = 'FP6008'
247
- }
262
+ INVALID_NUMERIC_OPERATION = 'FP6008',
263
+ VARIABLE_ALREADY_DEFINED = 'FP6009',
264
+ INVALID_TEMPORAL_UNIT = 'FP6010',
265
+ UNSUPPORTED_TEMPORAL_UNIT_FOR_TYPE = 'FP6011',
266
+
267
+ // Static analysis warnings (7000-7999)
268
+ UNREACHABLE_CODE = 'FP7001'
269
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Parser } from './parser';
2
- import { Interpreter, RuntimeContextManager } from './interpreter';
2
+ import { Interpreter } from './interpreter';
3
3
  import { Analyzer } from './analyzer';
4
4
  import type { AnalysisResult } from './types';
5
- import { box, unbox } from './boxing';
6
- import { FHIRPathError, Errors } from './errors';
5
+ import { DiagnosticSeverity } from './types';
6
+ import { FHIRPathError, Errors, ErrorCodes } from './errors';
7
7
 
8
8
  export interface EvaluateOptions {
9
9
  input?: unknown;
@@ -16,79 +16,13 @@ export async function evaluate(
16
16
  expression: string,
17
17
  options: EvaluateOptions = {}
18
18
  ): Promise<any[]> {
19
- const parser = new Parser(expression);
20
- const parseResult = parser.parse();
21
-
22
- // Check for parse errors
23
- if (parseResult.errors.length > 0) {
24
- // For backward compatibility, throw the first error
25
- const firstError = parseResult.errors[0]!;
26
- throw Errors.invalidSyntax(firstError.message);
27
- }
28
-
29
- // ALWAYS analyze the AST
30
- const analyzer = new Analyzer(options.modelProvider);
31
- const analysisResult = await analyzer.analyze(
32
- parseResult.ast,
33
- options.variables,
34
- options.inputType
35
- );
36
-
37
- // Check for analysis errors
38
- const errors = analysisResult.diagnostics.filter(d => d.severity === 1); // DiagnosticSeverity.Error
39
- if (errors.length > 0) {
40
- // Throw the first error
41
- const firstError = errors[0]!;
42
- if (firstError.code) {
43
- // Always throw as FHIRPathError if we have a code
44
- throw new FHIRPathError(firstError.code, firstError.message, firstError.range);
45
- } else {
46
- // Otherwise throw a generic error
47
- throw new Error(firstError.message);
48
- }
49
- }
50
-
51
- // Use the analyzed AST with type information
52
19
  const interpreter = new Interpreter(undefined, options.modelProvider);
53
- const input = options.input === undefined ? [] : Array.isArray(options.input) ? options.input : [options.input];
54
-
55
- // Box input with typeInfo if we have a modelProvider and the input is a FHIR resource
56
- let boxedInput = input;
57
- if (options.modelProvider) {
58
- boxedInput = await Promise.all(input.map(async item => {
59
- if (item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
60
- // Get type info asynchronously
61
- const typeInfo = await options.modelProvider!.getType(item.resourceType);
62
- if (typeInfo) {
63
- return box(item, typeInfo);
64
- }
65
- }
66
- return item;
67
- }));
68
- }
69
-
70
- // Create context with variables if provided
71
- let context = RuntimeContextManager.create(input);
72
-
73
- // Set $this to the boxed input (required for expressions like $this.where(...))
74
- context = RuntimeContextManager.setVariable(context, '$this', boxedInput);
75
-
76
- // Add model provider to context if available
77
- if (options.modelProvider) {
78
- context.modelProvider = options.modelProvider;
79
- }
80
-
81
- if (options.variables) {
82
- for (const [key, value] of Object.entries(options.variables)) {
83
- const varValue = Array.isArray(value) ? value : [value];
84
- context = RuntimeContextManager.setVariable(context, key, varValue);
85
- }
86
- }
87
-
88
- const result = await interpreter.evaluate(analysisResult.ast, input, context);
89
-
90
- // Unbox the results before returning
91
- return result.value.map(unbox);
20
+ return interpreter.evaluateExpression(expression, {
21
+ input: options.input,
22
+ variables: options.variables,
23
+ inputType: options.inputType,
24
+ modelProvider: options.modelProvider,
25
+ });
92
26
  }
93
27
 
94
28
  export async function analyze(
@@ -100,33 +34,12 @@ export async function analyze(
100
34
  errorRecovery?: boolean;
101
35
  } = {}
102
36
  ): Promise<AnalysisResult> {
103
- // Use LSP mode with error recovery if requested
104
- const parserOptions = options.errorRecovery
105
- ? { mode: 'lsp' as const, errorRecovery: true }
106
- : undefined;
107
-
108
- const parser = new Parser(expression, parserOptions);
109
- const parseResult = parser.parse();
110
-
111
- // Check for parse errors only if error recovery is disabled
112
- if (!options.errorRecovery && parseResult.errors.length > 0) {
113
- // For backward compatibility, throw the first error
114
- const firstError = parseResult.errors[0]!;
115
- throw Errors.invalidSyntax(firstError.message);
116
- }
117
-
118
- const ast = parseResult.ast;
119
-
120
- // Create analyzer with optional model provider
121
- const analyzer = new Analyzer(options.modelProvider);
122
- const analysisResult = await analyzer.analyze(ast, options.variables, options.inputType);
123
-
124
- // If error recovery is enabled, merge parse errors into diagnostics
125
- if (options.errorRecovery && parseResult.errors.length > 0) {
126
- // Parse errors are already converted to diagnostics by the analyzer
127
- // when it encounters Error nodes in the AST
128
- }
129
-
37
+ const analysisResult = await Analyzer.analyzeExpression(expression, {
38
+ variables: options.variables,
39
+ modelProvider: options.modelProvider,
40
+ inputType: options.inputType,
41
+ errorRecovery: options.errorRecovery,
42
+ });
130
43
  return analysisResult;
131
44
  }
132
45
 
@@ -183,7 +96,7 @@ export type {
183
96
  } from './completion-provider';
184
97
 
185
98
  // Export cursor node types for LSP integration
186
- export { CursorContext, isCursorNode } from './cursor-nodes';
99
+ export { CursorContext, isCursorNode } from './parser/cursor-nodes';
187
100
  export type {
188
101
  CursorNode,
189
102
  CursorOperatorNode,
@@ -192,4 +105,4 @@ export type {
192
105
  CursorIndexNode,
193
106
  CursorTypeNode,
194
107
  AnyCursorNode
195
- } from './cursor-nodes';
108
+ } from './parser/cursor-nodes';
package/src/inspect.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { TypeInfo, ASTNode, Diagnostic } from './types';
2
- import type { FHIRPathValue } from './boxing';
2
+ import type { FHIRPathValue } from './interpreter/boxing';
3
3
  import { NodeType, DiagnosticSeverity } from './types';
4
4
  import { parse } from './parser';
5
5
  import { Analyzer } from './analyzer';
6
- import { Interpreter, RuntimeContextManager } from './interpreter';
6
+ import { Interpreter } from './interpreter';
7
+ import { RuntimeContextManager } from './interpreter/runtime-context';
7
8
  import type { RuntimeContext } from './types';
8
9
 
9
10
  export interface ASTMetadata {
@@ -137,7 +138,6 @@ function analyzeAST(node: ASTNode, maxDepth = 100): ASTMetadata {
137
138
  case NodeType.Identifier:
138
139
  case NodeType.Variable:
139
140
  case NodeType.TypeReference:
140
- case NodeType.TypeOrIdentifier:
141
141
  complexity += 1;
142
142
  break;
143
143
  }
@@ -334,4 +334,4 @@ export async function inspect(
334
334
  },
335
335
  ...(options.includeTraces && { traces })
336
336
  };
337
- }
337
+ }
@@ -1,4 +1,4 @@
1
- import type {TypeInfo} from './types';
1
+ import type {TypeInfo} from '../types';
2
2
  export type {TypeInfo};
3
3
 
4
4
  /**
@@ -0,0 +1,94 @@
1
+ import type { ModelProvider, TypeInfo, TypeName } from '../types';
2
+ import { box, type FHIRPathValue } from './boxing';
3
+
4
+ interface ChoiceHit {
5
+ readonly value: any;
6
+ readonly typeInfo: TypeInfo;
7
+ readonly primitiveElement?: any;
8
+ }
9
+
10
+ function getPrimitiveElement(item: Record<string, unknown>, prop: string): any | undefined {
11
+ const primitiveElementName = `_${prop}`;
12
+ return Object.prototype.hasOwnProperty.call(item, primitiveElementName)
13
+ ? (item as any)[primitiveElementName]
14
+ : undefined;
15
+ }
16
+
17
+ async function maybeParseTemporal(
18
+ value: any,
19
+ expected: TypeInfo | undefined,
20
+ modelProvider?: ModelProvider
21
+ ): Promise<any> {
22
+ if (!modelProvider || !expected || typeof value !== 'string') {
23
+ return value;
24
+ }
25
+ if (expected.type === 'Date' || expected.type === 'DateTime' || expected.type === 'Time') {
26
+ const { parseTemporalLiteral } = await import('../complex-types/temporal');
27
+ return parseTemporalLiteral('@' + value);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ async function reboxResource(
33
+ value: any,
34
+ singleton: boolean,
35
+ modelProvider?: ModelProvider
36
+ ): Promise<FHIRPathValue> {
37
+ let resourceTypeInfo: TypeInfo | undefined;
38
+ if (modelProvider && typeof value?.resourceType === 'string') {
39
+ resourceTypeInfo = await modelProvider.getType(value.resourceType);
40
+ if (resourceTypeInfo) {
41
+ resourceTypeInfo = { ...resourceTypeInfo, singleton };
42
+ }
43
+ }
44
+ if (!resourceTypeInfo) {
45
+ // Default to 'Any' type when no provider or type info not found
46
+ resourceTypeInfo = { type: 'Any', singleton };
47
+ }
48
+ return box(value, resourceTypeInfo);
49
+ }
50
+
51
+ async function detectChoiceValues(
52
+ item: Record<string, unknown>,
53
+ base: string,
54
+ modelProvider?: ModelProvider
55
+ ): Promise<ChoiceHit[]> {
56
+ // Detect properties like baseXxx where Xxx is a type suffix
57
+ const possible = Object.keys(item).filter((k) => k.startsWith(base) && k !== base && k.length > base.length);
58
+ if (possible.length === 0) {
59
+ return [];
60
+ }
61
+ const hits: ChoiceHit[] = [];
62
+ for (const choiceProp of possible) {
63
+ const value = (item as any)[choiceProp];
64
+ if (value === null || value === undefined) {
65
+ continue;
66
+ }
67
+ const choiceName = choiceProp.substring(base.length);
68
+ const primitiveElement = getPrimitiveElement(item, choiceProp);
69
+ let choiceType: TypeInfo | undefined;
70
+ if (modelProvider) {
71
+ // Ask model provider for precise type if available; fallback to using suffix as TypeName
72
+ const providerType = await modelProvider.getType(choiceName);
73
+ if (providerType) {
74
+ choiceType = providerType;
75
+ }
76
+ }
77
+ if (!choiceType) {
78
+ choiceType = { type: choiceName as TypeName, singleton: !Array.isArray(value) };
79
+ } else {
80
+ choiceType = { ...choiceType, singleton: !Array.isArray(value) };
81
+ }
82
+ if (Array.isArray(value)) {
83
+ for (const v of value) {
84
+ hits.push({ value: v, typeInfo: { ...choiceType, singleton: true }, primitiveElement });
85
+ }
86
+ } else {
87
+ hits.push({ value, typeInfo: choiceType, primitiveElement });
88
+ }
89
+ }
90
+ return hits;
91
+ }
92
+
93
+ export { getPrimitiveElement, maybeParseTemporal, reboxResource, detectChoiceValues };
94
+
@@ -0,0 +1,273 @@
1
+ import { Errors } from '../errors';
2
+ import type { RuntimeContext } from '../types';
3
+ import { box } from './boxing';
4
+
5
+ // Temporal creators used for deterministic caches
6
+ import { createDateTime, createDate, createTime } from '../complex-types/temporal';
7
+
8
+ export interface BootstrapOptions {
9
+ modelProvider?: import('../types').ModelProvider;
10
+ variables?: Record<string, unknown>;
11
+ now?: Date; // provide deterministic time for tests
12
+ }
13
+
14
+ /**
15
+ * Runtime context manager that provides efficient prototype-based context operations
16
+ * for both interpreter and compiler.
17
+ */
18
+ export class RuntimeContextManager {
19
+ /**
20
+ * Create a new runtime context
21
+ */
22
+ static create(input: any[], initialVariables?: Record<string, any>): RuntimeContext {
23
+ const context = Object.create(null) as RuntimeContext;
24
+
25
+ context.input = input;
26
+ context.focus = input;
27
+
28
+ // Create variables object with null prototype to avoid pollution
29
+ context.variables = Object.create(null);
30
+
31
+ // Set root context variables with % prefix
32
+ context.variables['%context'] = input;
33
+ context.variables['%resource'] = input;
34
+ context.variables['%rootResource'] = input;
35
+
36
+ // Add any initial variables (with % prefix for user-defined)
37
+ if (initialVariables) {
38
+ for (const [key, value] of Object.entries(initialVariables)) {
39
+ // Add % prefix if not already present and not a special variable
40
+ const varKey = key.startsWith('$') || key.startsWith('%') ? key : `%${key}`;
41
+ context.variables[varKey] = value;
42
+ }
43
+ }
44
+
45
+ return context;
46
+ }
47
+
48
+ /**
49
+ * Create a child context using prototype inheritance
50
+ * O(1) operation - no copying needed
51
+ */
52
+ static copy(context: RuntimeContext): RuntimeContext {
53
+ // Create child context with parent as prototype
54
+ const newContext = Object.create(context) as RuntimeContext;
55
+
56
+ // Create child variables that inherit from parent's variables
57
+ newContext.variables = Object.create(context.variables);
58
+
59
+ // input and focus are inherited through prototype chain
60
+ // Only set them if they need to change
61
+
62
+ return newContext;
63
+ }
64
+
65
+ /**
66
+ * Create a new context with updated input/focus
67
+ */
68
+ static withInput(context: RuntimeContext, input: any[], focus?: any[]): RuntimeContext {
69
+ const newContext = this.copy(context);
70
+ newContext.input = input;
71
+ newContext.focus = focus ?? input;
72
+ return newContext;
73
+ }
74
+
75
+ /**
76
+ * Set iterator context ($this, $index)
77
+ */
78
+ static withIterator(
79
+ context: RuntimeContext,
80
+ item: any,
81
+ index: number
82
+ ): RuntimeContext {
83
+ let newContext = this.setVariable(context, '$this', [item], true);
84
+ newContext = this.setVariable(newContext, '$index', index, true);
85
+ return newContext;
86
+ }
87
+
88
+ /**
89
+ * Set a variable in the context (handles both special $ and user % variables)
90
+ */
91
+ static setVariable(context: RuntimeContext, name: string, value: any, allowRedefinition: boolean = false): RuntimeContext {
92
+ // Ensure value is array for consistency (except for special variables like $index)
93
+ const arrayValue = (name === '$index' || name === '$total') ? value :
94
+ Array.isArray(value) ? value : [value];
95
+
96
+ // Determine variable key based on prefix
97
+ let varKey = name;
98
+ if (!name.startsWith('$') && !name.startsWith('%')) {
99
+ // No prefix - assume user-defined variable, add % prefix
100
+ varKey = `%${name}`;
101
+ }
102
+
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
+ }
110
+
111
+ // Check if variable already exists (unless redefinition is allowed)
112
+ // Use 'in' operator to check prototype chain (inherited variables)
113
+ // Exclude iteration variables ($this, $index, $total) which can be redefined in nested scopes
114
+ const iterationVariables = ['$this', '$index', '$total'];
115
+ if (!allowRedefinition && context.variables && varKey in context.variables && !iterationVariables.includes(varKey)) {
116
+ // Per FHIRPath spec §1.5.10.3: throw error on variable redefinition
117
+ throw Errors.variableAlreadyDefined(name);
118
+ }
119
+
120
+ // Create new context and set variable
121
+ const newContext = this.copy(context);
122
+ newContext.variables[varKey] = arrayValue;
123
+
124
+ // Special handling for $this
125
+ if (varKey === '$this' && Array.isArray(arrayValue) && arrayValue.length === 1) {
126
+ newContext.input = arrayValue;
127
+ newContext.focus = arrayValue;
128
+ }
129
+
130
+ return newContext;
131
+ }
132
+
133
+ /**
134
+ * Get a variable from context
135
+ */
136
+ static getVariable(context: RuntimeContext, name: string): any | undefined {
137
+ // Handle special cases
138
+ if (name === '$this' || name === '$index' || name === '$total') {
139
+ return context.variables[name];
140
+ }
141
+
142
+ // Handle environment variables (with or without % prefix)
143
+ if (name === 'context' || name === '%context') {
144
+ return context.variables['%context'];
145
+ }
146
+ if (name === 'resource' || name === '%resource') {
147
+ return context.variables['%resource'];
148
+ }
149
+ if (name === 'rootResource' || name === '%rootResource') {
150
+ return context.variables['%rootResource'];
151
+ }
152
+
153
+ // Handle user-defined variables (add % prefix if not present)
154
+ const varKey = name.startsWith('%') ? name : `%${name}`;
155
+ // Use 'in' operator to check prototype chain for inherited variables
156
+ if (varKey in context.variables) {
157
+ return context.variables[varKey];
158
+ }
159
+ return undefined;
160
+ }
161
+
162
+ /**
163
+ * Bootstrap a runtime context with input, system variables, temporal caches,
164
+ * optional model provider, and user variables. Applies boxing policy for
165
+ * FHIR resources when a model provider is available.
166
+ */
167
+ static async bootstrapContext(
168
+ rawInput: unknown | unknown[],
169
+ options: BootstrapOptions = {}
170
+ ): Promise<{ context: RuntimeContext; input: any[] }> {
171
+ const { modelProvider, variables, now } = options;
172
+
173
+ // Normalize input to array
174
+ const inputArray = Array.isArray(rawInput)
175
+ ? rawInput
176
+ : rawInput === undefined || rawInput === null
177
+ ? []
178
+ : [rawInput];
179
+
180
+ // Box input with typeInfo when possible (FHIR resources)
181
+ let boxedInput = inputArray as any[];
182
+ if (modelProvider) {
183
+ boxedInput = await Promise.all(
184
+ inputArray.map(async (item) => {
185
+ if (
186
+ item &&
187
+ typeof item === 'object' &&
188
+ 'resourceType' in (item as any) &&
189
+ typeof (item as any).resourceType === 'string'
190
+ ) {
191
+ const ti = await modelProvider.getType((item as any).resourceType);
192
+ return ti ? box(item, ti) : item;
193
+ }
194
+ return item;
195
+ })
196
+ );
197
+ }
198
+
199
+ // Create context with BOXED input so system vars keep typeInfo
200
+ let context = RuntimeContextManager.create(boxedInput);
201
+
202
+ // Set $this to the boxed input (expressions may rely on $this)
203
+ context = RuntimeContextManager.setVariable(context, '$this', boxedInput);
204
+
205
+ // Pre-cache temporal values (single timestamp for now/today/timeOfDay)
206
+ const ts = now ?? new Date();
207
+ const dateTime = createDateTime(
208
+ ts.getFullYear(),
209
+ ts.getMonth() + 1,
210
+ ts.getDate(),
211
+ ts.getHours(),
212
+ ts.getMinutes(),
213
+ ts.getSeconds(),
214
+ ts.getMilliseconds(),
215
+ -ts.getTimezoneOffset()
216
+ );
217
+ context = RuntimeContextManager.setVariable(
218
+ context,
219
+ '__fhirpath_now_cache__',
220
+ box(dateTime, { type: 'DateTime', singleton: true })
221
+ );
222
+
223
+ const date = createDate(dateTime.year, dateTime.month, dateTime.day);
224
+ context = RuntimeContextManager.setVariable(
225
+ context,
226
+ '__fhirpath_today_cache__',
227
+ box(date, { type: 'Date', singleton: true })
228
+ );
229
+
230
+ const time = createTime(
231
+ dateTime.hour!,
232
+ dateTime.minute,
233
+ dateTime.second,
234
+ dateTime.millisecond
235
+ );
236
+ context = RuntimeContextManager.setVariable(
237
+ context,
238
+ '__fhirpath_timeOfDay_cache__',
239
+ box(time, { type: 'Time', singleton: true })
240
+ );
241
+
242
+ // Attach model provider to context
243
+ if (modelProvider) {
244
+ context.modelProvider = modelProvider;
245
+ }
246
+
247
+ // Add user variables, boxing FHIR resources when modelProvider present
248
+ if (variables) {
249
+ for (const [key, rawVal] of Object.entries(variables)) {
250
+ const values = Array.isArray(rawVal) ? rawVal : [rawVal];
251
+ const maybeBoxed = modelProvider
252
+ ? 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
+ )
266
+ : values;
267
+ context = RuntimeContextManager.setVariable(context, key, maybeBoxed);
268
+ }
269
+ }
270
+
271
+ return { context, input: boxedInput };
272
+ }
273
+ }