@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.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- 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 = '
|
|
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
|
|
2
|
+
import { Interpreter } from './interpreter';
|
|
3
3
|
import { Analyzer } from './analyzer';
|
|
4
4
|
import type { AnalysisResult } from './types';
|
|
5
|
-
import {
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|