@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/interpreter.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
228
|
+
let value: any = literal.value;
|
|
266
229
|
|
|
267
|
-
|
|
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(
|
|
258
|
+
value: [box(value, typeInfo)],
|
|
279
259
|
context
|
|
280
260
|
};
|
|
281
261
|
}
|
|
282
262
|
|
|
283
|
-
//
|
|
284
|
-
private
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
//
|
|
463
|
-
private async
|
|
464
|
-
const
|
|
465
|
-
const 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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
//
|
|
478
|
-
|
|
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
|
-
|
|
499
|
-
const rightResult = await
|
|
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('
|
|
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
|
-
|
|
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
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
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
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
//
|
|
650
|
-
|
|
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
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
//
|
|
719
|
-
|
|
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
|
|
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
|
+
}
|