@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900
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 +226 -120
- package/dist/index.js +11552 -5580
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
- 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 +939 -1204
- package/src/completion-provider.ts +209 -191
- package/src/complex-types/quantity-value.ts +410 -0
- package/src/complex-types/temporal.ts +1776 -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 +506 -468
- package/src/lexer.ts +192 -211
- 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 +99 -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 +744 -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 +132 -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/extension-function.ts +84 -0
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +7 -9
- package/src/operations/greater-or-equal-operator.ts +7 -9
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +193 -8
- package/src/operations/implies-operator.ts +2 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +43 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +70 -0
- package/src/operations/is-operator.ts +176 -13
- 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 +8 -9
- package/src/operations/less-or-equal-operator.ts +7 -9
- package/src/operations/less-than.ts +8 -13
- 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 +76 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +8 -2
- 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 +10 -3
- package/src/operations/ofType-function.ts +43 -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/precision-function.ts +146 -0
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +125 -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 +78 -15
- 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 +262 -503
- package/src/registry.ts +53 -42
- package/src/types.ts +129 -17
- package/src/utils/decimal.ts +76 -0
- package/src/utils/pprint.ts +151 -0
- package/src/quantity-value.ts +0 -198
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,226 +125,329 @@ 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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
250
|
+
// Use the valueType from the literal node to determine if it's integer or decimal
|
|
251
|
+
// This preserves the distinction between 1.0 (decimal) and 1 (integer)
|
|
252
|
+
typeInfo = literal.valueType === 'decimal' ?
|
|
253
|
+
{ type: 'Decimal', singleton: true } :
|
|
254
|
+
{ type: 'Integer', singleton: true };
|
|
273
255
|
} else if (typeof value === 'boolean') {
|
|
274
256
|
typeInfo = { type: 'Boolean', singleton: true };
|
|
275
257
|
}
|
|
276
258
|
|
|
277
259
|
return {
|
|
278
|
-
value: [box(
|
|
260
|
+
value: [box(value, typeInfo)],
|
|
279
261
|
context
|
|
280
262
|
};
|
|
281
263
|
}
|
|
282
264
|
|
|
283
|
-
//
|
|
284
|
-
private
|
|
285
|
-
|
|
286
|
-
|
|
265
|
+
// Helper: Handle extension elements
|
|
266
|
+
private handleExtension(
|
|
267
|
+
boxedItem: FHIRPathValue,
|
|
268
|
+
nodeTypeInfo?: TypeInfo
|
|
269
|
+
): FHIRPathValue[] {
|
|
270
|
+
const results: FHIRPathValue[] = [];
|
|
271
|
+
if (boxedItem.primitiveElement?.extension) {
|
|
272
|
+
for (const ext of boxedItem.primitiveElement.extension) {
|
|
273
|
+
results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return results;
|
|
277
|
+
}
|
|
287
278
|
|
|
288
|
-
|
|
279
|
+
// Helper: Handle FHIR choice types (e.g., value[x])
|
|
280
|
+
private async handleChoiceTypes(
|
|
281
|
+
item: object,
|
|
282
|
+
name: string,
|
|
283
|
+
context: RuntimeContext
|
|
284
|
+
): Promise<FHIRPathValue[]> {
|
|
289
285
|
const results: FHIRPathValue[] = [];
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
286
|
+
const choiceHits = await detectChoiceValues(item as Record<string, unknown>, name, context.modelProvider);
|
|
287
|
+
for (const hit of choiceHits) {
|
|
288
|
+
results.push(box(hit.value, hit.typeInfo, hit.primitiveElement));
|
|
289
|
+
}
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Helper: Handle union type choices
|
|
294
|
+
private handleUnionChoices(
|
|
295
|
+
item: object,
|
|
296
|
+
nodeTypeInfo?: TypeInfo
|
|
297
|
+
): FHIRPathValue[] {
|
|
298
|
+
const results: FHIRPathValue[] = [];
|
|
299
|
+
if (
|
|
300
|
+
nodeTypeInfo?.modelContext &&
|
|
301
|
+
typeof nodeTypeInfo.modelContext === 'object' &&
|
|
302
|
+
'isUnion' in nodeTypeInfo.modelContext &&
|
|
303
|
+
(nodeTypeInfo.modelContext as any).isUnion &&
|
|
304
|
+
'choices' in nodeTypeInfo.modelContext &&
|
|
305
|
+
Array.isArray((nodeTypeInfo.modelContext as any).choices)
|
|
306
|
+
) {
|
|
307
|
+
for (const choice of (nodeTypeInfo.modelContext as any).choices) {
|
|
308
|
+
const choiceName = choice.choiceName;
|
|
309
|
+
if (choiceName && choiceName in (item as any)) {
|
|
310
|
+
const value = (item as any)[choiceName];
|
|
311
|
+
const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, choiceName);
|
|
312
|
+
const choiceTypeInfo = { type: choice.type, singleton: !Array.isArray(value), modelContext: choice } as any;
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
for (const v of value) {
|
|
315
|
+
results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
|
|
316
|
+
}
|
|
317
|
+
} else if (value !== null && value !== undefined) {
|
|
318
|
+
results.push(box(value, choiceTypeInfo, primitiveElement));
|
|
319
|
+
}
|
|
302
320
|
}
|
|
303
|
-
continue;
|
|
304
321
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Helper: Handle standard property access
|
|
327
|
+
private async handleStandardProperty(
|
|
328
|
+
item: object,
|
|
329
|
+
name: string,
|
|
330
|
+
nodeTypeInfo: TypeInfo | undefined,
|
|
331
|
+
context: RuntimeContext,
|
|
332
|
+
parentTypeInfo?: TypeInfo
|
|
333
|
+
): Promise<FHIRPathValue[]> {
|
|
334
|
+
const results: FHIRPathValue[] = [];
|
|
335
|
+
if (name in (item as any)) {
|
|
336
|
+
const value = (item as any)[name];
|
|
337
|
+
const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, name);
|
|
338
|
+
|
|
339
|
+
// Determine if this is a FHIR primitive - if parent is a FHIR resource and value is primitive
|
|
340
|
+
const isFHIRPrimitive = parentTypeInfo &&
|
|
341
|
+
parentTypeInfo.type &&
|
|
342
|
+
parentTypeInfo.type !== 'Any' &&
|
|
343
|
+
!parentTypeInfo.type.startsWith('System.') &&
|
|
344
|
+
(typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number');
|
|
345
|
+
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
let elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
|
|
310
348
|
|
|
311
|
-
//
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
key.startsWith(name) && key !== name && key.length > name.length
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
if (possibleChoiceProperties.length > 0) {
|
|
319
|
-
// This looks like a choice type - return all matching values
|
|
320
|
-
for (const choiceProp of possibleChoiceProperties) {
|
|
321
|
-
const value = item[choiceProp];
|
|
322
|
-
const primitiveElementName = `_${choiceProp}`;
|
|
323
|
-
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
|
|
324
|
-
|
|
325
|
-
// Try to determine the type from the property name suffix
|
|
326
|
-
let choiceType = 'Any';
|
|
327
|
-
const suffix = choiceProp.substring(name.length);
|
|
328
|
-
if (suffix) {
|
|
329
|
-
// Remove leading uppercase letter and make it the type
|
|
330
|
-
choiceType = suffix;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (Array.isArray(value)) {
|
|
334
|
-
for (const v of value) {
|
|
335
|
-
// For FHIR resources, use their resourceType
|
|
336
|
-
if (v && typeof v === 'object' && 'resourceType' in v) {
|
|
337
|
-
const typeInfo = await context.modelProvider!.getType(v.resourceType);
|
|
338
|
-
results.push(box(v, typeInfo || { type: v.resourceType as any, singleton: true }, primitiveElement));
|
|
339
|
-
} else {
|
|
340
|
-
results.push(box(v, { type: choiceType as any, singleton: true }, primitiveElement));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} else if (value !== null && value !== undefined) {
|
|
344
|
-
// For FHIR resources, use their resourceType
|
|
345
|
-
if (value && typeof value === 'object' && 'resourceType' in value) {
|
|
346
|
-
const typeInfo = await context.modelProvider!.getType(value.resourceType);
|
|
347
|
-
results.push(box(value, typeInfo || { type: value.resourceType as any, singleton: true }, primitiveElement));
|
|
348
|
-
} else {
|
|
349
|
-
results.push(box(value, { type: choiceType as any, singleton: !Array.isArray(value) }, primitiveElement));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
foundChoiceValue = true;
|
|
353
|
-
}
|
|
349
|
+
// For FHIR primitives, use FHIR namespace
|
|
350
|
+
if (isFHIRPrimitive && elementTypeInfo) {
|
|
351
|
+
if (elementTypeInfo.type === 'Boolean') {
|
|
352
|
+
elementTypeInfo = { ...elementTypeInfo, type: 'boolean' as any };
|
|
354
353
|
}
|
|
355
354
|
}
|
|
356
355
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
}
|
|
356
|
+
for (const v of value) {
|
|
357
|
+
if (
|
|
358
|
+
v && typeof v === 'object' && 'resourceType' in (v as any) && typeof (v as any).resourceType === 'string'
|
|
359
|
+
) {
|
|
360
|
+
// Always re-box FHIR resources to get proper type information from ModelProvider
|
|
361
|
+
const boxed = await reboxResource(v, true, context.modelProvider);
|
|
362
|
+
results.push(boxed);
|
|
363
|
+
} else {
|
|
364
|
+
const val = await maybeParseTemporal(v, elementTypeInfo, context.modelProvider);
|
|
365
|
+
results.push(box(val, elementTypeInfo, primitiveElement));
|
|
386
366
|
}
|
|
387
367
|
}
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
}
|
|
424
|
-
}
|
|
425
|
-
} else if (value !== null && value !== undefined) {
|
|
426
|
-
// Special handling for FHIR resources - use their resourceType
|
|
427
|
-
// Do this when the property could be polymorphic (type is 'Any' or 'Resource')
|
|
428
|
-
if (value && typeof value === 'object' && 'resourceType' in value && typeof value.resourceType === 'string' &&
|
|
429
|
-
(!nodeTypeInfo || nodeTypeInfo.type === 'Any' || (nodeTypeInfo as any).type === 'Resource')) {
|
|
430
|
-
// Get full type info from model provider if available
|
|
431
|
-
let resourceTypeInfo;
|
|
432
|
-
if (context.modelProvider) {
|
|
433
|
-
resourceTypeInfo = await context.modelProvider.getType(value.resourceType);
|
|
434
|
-
if (resourceTypeInfo) {
|
|
435
|
-
// Preserve singleton status
|
|
436
|
-
resourceTypeInfo = { ...resourceTypeInfo, singleton: !Array.isArray(value) };
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (!resourceTypeInfo) {
|
|
440
|
-
// Fallback to basic type info
|
|
441
|
-
resourceTypeInfo = {
|
|
442
|
-
type: value.resourceType as import('./types').TypeName,
|
|
443
|
-
singleton: !Array.isArray(value)
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
results.push(box(value, resourceTypeInfo, primitiveElement));
|
|
447
|
-
} else {
|
|
448
|
-
// Box single value with primitive element if available
|
|
449
|
-
results.push(box(value, nodeTypeInfo, primitiveElement));
|
|
368
|
+
} else if (value !== null && value !== undefined) {
|
|
369
|
+
if (
|
|
370
|
+
value && typeof value === 'object' && 'resourceType' in (value as any) && typeof (value as any).resourceType === 'string'
|
|
371
|
+
) {
|
|
372
|
+
// Always re-box FHIR resources to get proper type information from ModelProvider
|
|
373
|
+
const boxed = await reboxResource(value, true, context.modelProvider);
|
|
374
|
+
results.push(boxed);
|
|
375
|
+
} else {
|
|
376
|
+
// For FHIR primitives, use FHIR namespace
|
|
377
|
+
let typeInfo = nodeTypeInfo;
|
|
378
|
+
if (isFHIRPrimitive && typeInfo) {
|
|
379
|
+
if (typeInfo.type === 'Boolean') {
|
|
380
|
+
typeInfo = { ...typeInfo, type: 'boolean' as any };
|
|
450
381
|
}
|
|
451
382
|
}
|
|
383
|
+
|
|
384
|
+
const val = await maybeParseTemporal(value, typeInfo, context.modelProvider);
|
|
385
|
+
results.push(box(val, typeInfo, primitiveElement));
|
|
452
386
|
}
|
|
453
387
|
}
|
|
454
388
|
}
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
value: results,
|
|
458
|
-
context
|
|
459
|
-
};
|
|
389
|
+
return results;
|
|
460
390
|
}
|
|
461
391
|
|
|
462
|
-
//
|
|
463
|
-
private async
|
|
464
|
-
const
|
|
465
|
-
const name =
|
|
392
|
+
// Identifier node evaluator
|
|
393
|
+
private async evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
394
|
+
const identifier = node as IdentifierNode;
|
|
395
|
+
const name = identifier.name;
|
|
396
|
+
const nodeTypeInfo = node.typeInfo;
|
|
397
|
+
const results: FHIRPathValue[] = [];
|
|
466
398
|
|
|
467
|
-
|
|
468
|
-
const filtered = input.filter(boxedItem => {
|
|
399
|
+
for (const boxedItem of input) {
|
|
469
400
|
const item = unbox(boxedItem);
|
|
470
|
-
return item && typeof item === 'object' && item.resourceType === name;
|
|
471
|
-
});
|
|
472
401
|
|
|
473
|
-
|
|
474
|
-
|
|
402
|
+
// 1. Handle extension special case
|
|
403
|
+
if (name === 'extension') {
|
|
404
|
+
results.push(...this.handleExtension(boxedItem, nodeTypeInfo));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Process only objects
|
|
409
|
+
if (item && typeof item === 'object') {
|
|
410
|
+
// 2. Handle FHIR choice types (e.g., value[x])
|
|
411
|
+
const choiceResults = await this.handleChoiceTypes(item, name, context);
|
|
412
|
+
if (choiceResults.length > 0) {
|
|
413
|
+
results.push(...choiceResults);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 3. Handle union type choices
|
|
418
|
+
const unionResults = this.handleUnionChoices(item, nodeTypeInfo);
|
|
419
|
+
results.push(...unionResults);
|
|
420
|
+
|
|
421
|
+
// 4. Handle standard property access
|
|
422
|
+
const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context, boxedItem.typeInfo);
|
|
423
|
+
results.push(...propertyResults);
|
|
424
|
+
}
|
|
475
425
|
}
|
|
476
426
|
|
|
477
|
-
//
|
|
478
|
-
|
|
427
|
+
// If no properties matched, try type-filter fallback on resources
|
|
428
|
+
if (results.length === 0) {
|
|
429
|
+
const filtered: FHIRPathValue[] = [];
|
|
430
|
+
for (const boxedItem of input) {
|
|
431
|
+
const item = unbox(boxedItem);
|
|
432
|
+
if (item && typeof item === 'object' && (item as any).resourceType === name) {
|
|
433
|
+
if (context.modelProvider) {
|
|
434
|
+
const typeInfo = await context.modelProvider.getType(name);
|
|
435
|
+
if (typeInfo) {
|
|
436
|
+
filtered.push(box(item, { ...typeInfo, singleton: true }));
|
|
437
|
+
} else {
|
|
438
|
+
filtered.push(boxedItem);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
filtered.push(boxedItem);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (filtered.length > 0) {
|
|
446
|
+
return { value: filtered, context };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { value: results, context };
|
|
479
451
|
}
|
|
480
452
|
|
|
481
453
|
// Binary operator evaluator
|
|
@@ -485,8 +457,29 @@ export class Interpreter {
|
|
|
485
457
|
|
|
486
458
|
// Special handling for dot operator (sequential pipeline)
|
|
487
459
|
if (operator === '.') {
|
|
460
|
+
// Check if this is actually a namespaced type in an 'is' expression
|
|
461
|
+
// Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
|
|
462
|
+
if (binary.left.type === NodeType.MembershipTest && binary.right.type === NodeType.Identifier) {
|
|
463
|
+
const membershipTest = binary.left as MembershipTestNode;
|
|
464
|
+
const rightIdent = binary.right as IdentifierNode;
|
|
465
|
+
|
|
466
|
+
// Extract the expression from the membership test
|
|
467
|
+
const expr = membershipTest.expression;
|
|
468
|
+
const typeName = `${membershipTest.targetType}.${rightIdent.name}`;
|
|
469
|
+
|
|
470
|
+
// Evaluate the expression
|
|
471
|
+
const exprResult = await this.evaluate(expr, input, context);
|
|
472
|
+
|
|
473
|
+
// Now apply the is operator with the full type name
|
|
474
|
+
const evaluator = this.operationEvaluators.get('is');
|
|
475
|
+
if (evaluator) {
|
|
476
|
+
return await evaluator(input, context, exprResult.value, [typeName]);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
488
480
|
// Evaluate left with current input/context
|
|
489
481
|
const leftResult = await this.evaluate(binary.left, input, context);
|
|
482
|
+
|
|
490
483
|
// Use left's output as right's input, and left's context flows to right
|
|
491
484
|
return await this.evaluate(binary.right, leftResult.value, leftResult.context);
|
|
492
485
|
}
|
|
@@ -495,28 +488,120 @@ export class Interpreter {
|
|
|
495
488
|
if (operator === '|') {
|
|
496
489
|
// Each side of union should have its own variable scope
|
|
497
490
|
// Variables defined on left side should not be visible on right side
|
|
498
|
-
|
|
499
|
-
const rightResult = await
|
|
491
|
+
// Evaluate both sides in parallel since both use the same input/context
|
|
492
|
+
const [leftResult, rightResult] = await Promise.all([
|
|
493
|
+
this.evaluate(binary.left, input, context),
|
|
494
|
+
this.evaluate(binary.right, input, context)
|
|
495
|
+
]);
|
|
500
496
|
|
|
501
497
|
// Merge the results
|
|
502
|
-
const unionEvaluator = this.operationEvaluators.get('
|
|
498
|
+
const unionEvaluator = this.operationEvaluators.get('|');
|
|
503
499
|
if (unionEvaluator) {
|
|
504
500
|
return await unionEvaluator(input, context, leftResult.value, rightResult.value);
|
|
505
501
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return {
|
|
509
|
-
value: [...leftResult.value, ...rightResult.value],
|
|
510
|
-
context // Original context preserved
|
|
511
|
-
};
|
|
502
|
+
// If union evaluator not found, surface a clear error
|
|
503
|
+
throw Errors.noEvaluatorFound('binary operator', '|');
|
|
512
504
|
}
|
|
513
505
|
|
|
506
|
+
// Special handling for 'is' and 'as' operators - right side is a type identifier, not an expression
|
|
507
|
+
if (operator === 'is' || operator === 'as') {
|
|
508
|
+
const leftResult = await this.evaluate(binary.left, input, context);
|
|
509
|
+
|
|
510
|
+
// Extract type name from right side WITHOUT evaluating it
|
|
511
|
+
let typeName: string;
|
|
512
|
+
if (binary.right.type === NodeType.Identifier) {
|
|
513
|
+
typeName = (binary.right as any).name;
|
|
514
|
+
} else if (binary.right.type === NodeType.Binary && (binary.right as any).operator === '.') {
|
|
515
|
+
// Handle namespaced types like System.Boolean or FHIR.Patient
|
|
516
|
+
const rightBinary = binary.right as any;
|
|
517
|
+
if (rightBinary.left.type === NodeType.Identifier && rightBinary.right.type === NodeType.Identifier) {
|
|
518
|
+
typeName = `${rightBinary.left.name}.${rightBinary.right.name}`;
|
|
519
|
+
} else {
|
|
520
|
+
throw new Error('is operator requires a type name as right operand');
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
throw new Error('is operator requires a type name as right operand');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const evaluator = this.operationEvaluators.get('is');
|
|
527
|
+
if (evaluator) {
|
|
528
|
+
return await evaluator(input, context, leftResult.value, [typeName]);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
514
532
|
// Get operation evaluator
|
|
515
533
|
const evaluator = this.operationEvaluators.get(operator);
|
|
516
534
|
if (evaluator) {
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
535
|
+
// Short-circuit for logical operators when possible
|
|
536
|
+
if (operator === 'and' || operator === 'or' || operator === 'implies') {
|
|
537
|
+
const leftResult = await this.evaluate(binary.left, input, context);
|
|
538
|
+
const kind = this.getBooleanKind(leftResult.value);
|
|
539
|
+
|
|
540
|
+
if (operator === 'and') {
|
|
541
|
+
// false and _ -> false (short-circuit)
|
|
542
|
+
if (kind === 'false') {
|
|
543
|
+
return { value: [this.boxBoolean(false)], context };
|
|
544
|
+
}
|
|
545
|
+
// true and y -> y; empty and false -> false handled by evaluator; need right
|
|
546
|
+
const rightResult = await this.evaluate(binary.right, input, context);
|
|
547
|
+
const operatorDef = this.registry.getOperatorDefinition(operator);
|
|
548
|
+
if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
|
|
549
|
+
if (leftResult.value.length === 0 || rightResult.value.length === 0) {
|
|
550
|
+
return { value: [], context };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return await evaluator(input, context, leftResult.value, rightResult.value);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (operator === 'or') {
|
|
557
|
+
// true or _ -> true (short-circuit)
|
|
558
|
+
if (kind === 'true') {
|
|
559
|
+
return { value: [this.boxBoolean(true)], context };
|
|
560
|
+
}
|
|
561
|
+
// false or y -> y; empty or true -> true handled by evaluator; need right
|
|
562
|
+
const rightResult = await this.evaluate(binary.right, input, context);
|
|
563
|
+
const operatorDef = this.registry.getOperatorDefinition(operator);
|
|
564
|
+
if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
|
|
565
|
+
if (leftResult.value.length === 0 || rightResult.value.length === 0) {
|
|
566
|
+
return { value: [], context };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return await evaluator(input, context, leftResult.value, rightResult.value);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (operator === 'implies') {
|
|
573
|
+
// false implies _ -> true (short-circuit)
|
|
574
|
+
if (kind === 'false') {
|
|
575
|
+
return { value: [this.boxBoolean(true)], context };
|
|
576
|
+
}
|
|
577
|
+
// true implies y -> y; empty implies y -> true if y true else empty; need right
|
|
578
|
+
const rightResult = await this.evaluate(binary.right, input, context);
|
|
579
|
+
const operatorDef = this.registry.getOperatorDefinition(operator);
|
|
580
|
+
if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
|
|
581
|
+
if (leftResult.value.length === 0 || rightResult.value.length === 0) {
|
|
582
|
+
return { value: [], context };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return await evaluator(input, context, leftResult.value, rightResult.value);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Default path: evaluate both operands in parallel
|
|
590
|
+
const [leftResult, rightResult] = await Promise.all([
|
|
591
|
+
this.evaluate(binary.left, input, context),
|
|
592
|
+
this.evaluate(binary.right, input, context)
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
// Handle empty propagation for operators
|
|
596
|
+
// Rely exclusively on registry metadata
|
|
597
|
+
const operatorDef = this.registry.getOperatorDefinition(operator);
|
|
598
|
+
if (operatorDef && !operatorDef.doesNotPropagateEmpty) {
|
|
599
|
+
// Check if either operand is empty
|
|
600
|
+
if (leftResult.value.length === 0 || rightResult.value.length === 0) {
|
|
601
|
+
return { value: [], context };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
520
605
|
return await evaluator(input, context, leftResult.value, rightResult.value);
|
|
521
606
|
}
|
|
522
607
|
|
|
@@ -584,14 +669,83 @@ export class Interpreter {
|
|
|
584
669
|
const func = node as FunctionNode;
|
|
585
670
|
const funcName = (func.name as IdentifierNode).name;
|
|
586
671
|
|
|
672
|
+
// Get the function definition to check if it propagates empty
|
|
673
|
+
const functionDef = this.registry.getFunction(funcName);
|
|
674
|
+
|
|
587
675
|
// Check if function is registered with an evaluator
|
|
588
676
|
const functionEvaluator = this.functionEvaluators.get(funcName);
|
|
589
|
-
if (functionEvaluator) {
|
|
590
|
-
|
|
677
|
+
if (!functionEvaluator) {
|
|
678
|
+
// No function found in registry
|
|
679
|
+
throw Errors.unknownFunction(funcName);
|
|
591
680
|
}
|
|
681
|
+
// Helper: pick a matching signature based on argument count
|
|
682
|
+
const pickSignature = () => {
|
|
683
|
+
if (!functionDef?.signatures || functionDef.signatures.length === 0) {
|
|
684
|
+
return undefined as import('./types').FunctionSignature | undefined;
|
|
685
|
+
}
|
|
686
|
+
const argsCount = func.arguments.length;
|
|
687
|
+
for (const sig of functionDef.signatures) {
|
|
688
|
+
const total = sig.parameters.length;
|
|
689
|
+
const required = sig.parameters.filter(p => !p.optional).length;
|
|
690
|
+
if (argsCount >= required && argsCount <= total) {
|
|
691
|
+
return sig;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Fallback to first signature
|
|
695
|
+
return functionDef.signatures[0];
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const signature = pickSignature();
|
|
699
|
+
|
|
700
|
+
// Memoized evaluator to avoid duplicate evaluation of arguments
|
|
701
|
+
// Only memoize when both input and context references are identical to this call's input/context.
|
|
702
|
+
const originalInputRef = input;
|
|
703
|
+
const originalContextRef = context;
|
|
704
|
+
const cache = new WeakMap<ASTNode, Promise<EvaluationResult>>();
|
|
705
|
+
const memoEval = async (n: ASTNode, inVals: any[], ctx: RuntimeContext) => {
|
|
706
|
+
if (inVals === originalInputRef && ctx === originalContextRef) {
|
|
707
|
+
const cached = cache.get(n);
|
|
708
|
+
if (cached) {
|
|
709
|
+
return cached;
|
|
710
|
+
}
|
|
711
|
+
const promise = this.evaluate(n, inVals, ctx);
|
|
712
|
+
cache.set(n, promise);
|
|
713
|
+
return promise;
|
|
714
|
+
}
|
|
715
|
+
// Different input or context – do not reuse cached result
|
|
716
|
+
return this.evaluate(n, inVals, ctx);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// Handle empty propagation centrally (default: propagate)
|
|
720
|
+
if (functionDef && !functionDef.doesNotPropagateEmpty) {
|
|
721
|
+
// If input is empty, propagate empty immediately
|
|
722
|
+
if (input.length === 0) {
|
|
723
|
+
return { value: [], context };
|
|
724
|
+
}
|
|
592
725
|
|
|
593
|
-
|
|
594
|
-
|
|
726
|
+
// Evaluate non-expression, non-typeReference arguments once for emptiness
|
|
727
|
+
for (let i = 0; i < func.arguments.length; i++) {
|
|
728
|
+
const arg = func.arguments[i];
|
|
729
|
+
const param = signature?.parameters[i];
|
|
730
|
+
if (!arg || !param) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
// Skip expression or typeReference params (not evaluated here)
|
|
734
|
+
if (param.expression || param.typeReference) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
// Evaluate with memoization
|
|
738
|
+
const argResult = await memoEval(arg, input, context);
|
|
739
|
+
// If argument is empty and it's a required parameter, propagate empty
|
|
740
|
+
const isRequired = !param.optional;
|
|
741
|
+
if (isRequired && argResult.value.length === 0) {
|
|
742
|
+
return { value: [], context };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Call the function evaluator with memoized evaluator
|
|
748
|
+
return await functionEvaluator(input, context, func.arguments, memoEval);
|
|
595
749
|
}
|
|
596
750
|
|
|
597
751
|
// Index evaluator
|
|
@@ -621,76 +775,14 @@ export class Interpreter {
|
|
|
621
775
|
const test = node as MembershipTestNode;
|
|
622
776
|
const exprResult = await this.evaluate(test.expression, input, context);
|
|
623
777
|
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
778
|
+
// Use the is-operator implementation for consistency
|
|
779
|
+
const isOperator = this.operationEvaluators.get('is');
|
|
780
|
+
if (isOperator) {
|
|
781
|
+
return isOperator(input, context, exprResult.value, [test.targetType]);
|
|
627
782
|
}
|
|
628
783
|
|
|
629
|
-
//
|
|
630
|
-
|
|
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
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Type checking with subtype support via ModelProvider
|
|
650
|
-
const results = await Promise.all(exprResult.value.map(async boxedItem => {
|
|
651
|
-
const item = unbox(boxedItem);
|
|
652
|
-
|
|
653
|
-
// If we have a ModelProvider and typeInfo, use it for accurate subtype checking
|
|
654
|
-
if (context.modelProvider && boxedItem.typeInfo) {
|
|
655
|
-
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, test.targetType as import('./types').TypeName);
|
|
656
|
-
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// For FHIR resources without typeInfo, try to get it from modelProvider
|
|
660
|
-
if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
|
|
661
|
-
const typeInfo = await context.modelProvider.getType(item.resourceType);
|
|
662
|
-
if (typeInfo) {
|
|
663
|
-
const matchingType = context.modelProvider.ofType(typeInfo, test.targetType as import('./types').TypeName);
|
|
664
|
-
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
|
|
665
|
-
}
|
|
666
|
-
// Fall back to exact match
|
|
667
|
-
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Check for FHIR resource types (no ModelProvider available)
|
|
671
|
-
if (item && typeof item === 'object' && 'resourceType' in item) {
|
|
672
|
-
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Check primitive types
|
|
676
|
-
const isMatch = (() => {
|
|
677
|
-
switch (test.targetType) {
|
|
678
|
-
case 'String': return typeof item === 'string';
|
|
679
|
-
case 'Boolean': return typeof item === 'boolean';
|
|
680
|
-
case 'Integer': return Number.isInteger(item);
|
|
681
|
-
case 'Decimal': return typeof item === 'number';
|
|
682
|
-
case 'Date':
|
|
683
|
-
case 'DateTime':
|
|
684
|
-
case 'Time':
|
|
685
|
-
// Simple check for date-like strings
|
|
686
|
-
return typeof item === 'string' && !isNaN(Date.parse(item));
|
|
687
|
-
default: return false;
|
|
688
|
-
}
|
|
689
|
-
})();
|
|
690
|
-
return box(isMatch, { type: 'Boolean', singleton: true });
|
|
691
|
-
}));
|
|
692
|
-
|
|
693
|
-
return { value: results, context };
|
|
784
|
+
// Fallback - shouldn't reach here normally
|
|
785
|
+
return { value: [], context };
|
|
694
786
|
}
|
|
695
787
|
|
|
696
788
|
// Type cast (as operator)
|
|
@@ -698,76 +790,22 @@ export class Interpreter {
|
|
|
698
790
|
const cast = node as TypeCastNode;
|
|
699
791
|
const exprResult = await this.evaluate(cast.expression, input, context);
|
|
700
792
|
|
|
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
|
-
}
|
|
793
|
+
// Use the as-operator implementation for consistency
|
|
794
|
+
const asOperator = this.operationEvaluators.get('as');
|
|
795
|
+
if (asOperator) {
|
|
796
|
+
return asOperator(input, context, exprResult.value, [cast.targetType]);
|
|
716
797
|
}
|
|
717
798
|
|
|
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 };
|
|
799
|
+
// Fallback implementation (shouldn't normally reach here)
|
|
800
|
+
return { value: [], context };
|
|
763
801
|
}
|
|
764
802
|
|
|
765
803
|
private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
766
804
|
const quantity = node as QuantityNode;
|
|
767
|
-
const quantityValue = createQuantity(quantity.value, quantity.unit
|
|
805
|
+
const quantityValue = createQuantity(quantity.value, quantity.unit);
|
|
768
806
|
return {
|
|
769
807
|
value: [box(quantityValue, { type: 'Quantity', singleton: true })],
|
|
770
808
|
context
|
|
771
809
|
};
|
|
772
810
|
}
|
|
773
|
-
}
|
|
811
|
+
}
|