@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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// dayOf() function - Extracts day component from Date or DateTime
|
|
2
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import { isFHIRDate, isFHIRDateTime } from '../complex-types/temporal';
|
|
5
|
+
import { Errors } from '../errors';
|
|
6
|
+
|
|
7
|
+
export const dayOfEvaluator: FunctionEvaluator = async (input, context, args) => {
|
|
8
|
+
// dayOf() takes no arguments
|
|
9
|
+
if (args.length !== 0) {
|
|
10
|
+
throw Errors.wrongArgumentCount('dayOf', 0, args.length);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Empty input returns empty
|
|
14
|
+
if (input.length === 0) {
|
|
15
|
+
return { value: [], context };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Multiple items throws error
|
|
19
|
+
if (input.length > 1) {
|
|
20
|
+
throw Errors.singletonRequired('dayOf', input.length);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const boxedValue = input[0];
|
|
24
|
+
if (!boxedValue) {
|
|
25
|
+
return { value: [], context };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const value = unbox(boxedValue);
|
|
29
|
+
|
|
30
|
+
// Check if it's a Date or DateTime
|
|
31
|
+
if (isFHIRDate(value) || isFHIRDateTime(value)) {
|
|
32
|
+
// Check if day component is present
|
|
33
|
+
if (value.day === undefined) {
|
|
34
|
+
return { value: [], context };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the day as an Integer (1-31)
|
|
38
|
+
return {
|
|
39
|
+
value: [box(value.day, { type: 'Integer', singleton: true })],
|
|
40
|
+
context
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Not a Date or DateTime, return empty
|
|
45
|
+
return { value: [], context };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const dayOfFunction: FunctionDefinition & { evaluate: typeof dayOfEvaluator } = {
|
|
49
|
+
name: 'dayOf',
|
|
50
|
+
category: ['temporal'],
|
|
51
|
+
description: 'Returns the day component of a Date or DateTime value (1-31)',
|
|
52
|
+
examples: [
|
|
53
|
+
'@2014-01-05.dayOf()',
|
|
54
|
+
'@2014-01-05T10:30:00.dayOf()',
|
|
55
|
+
'Patient.birthDate.dayOf()'
|
|
56
|
+
],
|
|
57
|
+
signatures: [
|
|
58
|
+
{
|
|
59
|
+
name: 'dayOf',
|
|
60
|
+
input: { type: 'Any', singleton: true },
|
|
61
|
+
parameters: [],
|
|
62
|
+
result: { type: 'Integer', singleton: true }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
evaluate: dayOfEvaluator
|
|
66
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decimal boundary functions for FHIRPath
|
|
3
|
+
*
|
|
4
|
+
* Implements lowBoundary and highBoundary for decimal values according to the FHIRPath specification.
|
|
5
|
+
* These functions return the least or greatest possible value at a specified precision.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Maximum supported precision for decimal values (FHIRPath requires at least 8)
|
|
10
|
+
*/
|
|
11
|
+
const MAX_DECIMAL_PRECISION = 8;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the low boundary of a decimal value at the specified precision
|
|
15
|
+
*
|
|
16
|
+
* Returns the least possible value of the input to the specified precision.
|
|
17
|
+
* For the given precision, returns the truncated value minus half the step size.
|
|
18
|
+
*
|
|
19
|
+
* @param value The decimal value
|
|
20
|
+
* @param precision The number of decimal places (optional, defaults to input precision + 1)
|
|
21
|
+
* @returns The low boundary value, or null if precision is invalid
|
|
22
|
+
*/
|
|
23
|
+
export function getDecimalLowBoundary(value: number, precision?: number): number | null {
|
|
24
|
+
// Default precision is the input's precision (for decimals) or 1 (for integers)
|
|
25
|
+
if (precision === undefined) {
|
|
26
|
+
const str = value.toString();
|
|
27
|
+
const decimalIndex = str.indexOf('.');
|
|
28
|
+
const inputPrecision = decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
|
|
29
|
+
// For integers, default to precision 1; for decimals, use their precision
|
|
30
|
+
precision = inputPrecision === 0 ? 1 : Math.min(inputPrecision, MAX_DECIMAL_PRECISION);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Invalid precision returns null (empty in FHIRPath)
|
|
34
|
+
if (precision < 0 || precision > MAX_DECIMAL_PRECISION) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For precision 0 (integer precision)
|
|
39
|
+
if (precision === 0) {
|
|
40
|
+
// For integers at precision 0, return floor - 1 for positive, ceiling + 1 for negative
|
|
41
|
+
// 1.lowBoundary(0) = 0
|
|
42
|
+
// -1.lowBoundary(0) = -2
|
|
43
|
+
const truncated = value >= 0 ? Math.floor(value) : Math.ceil(value);
|
|
44
|
+
return truncated - 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Calculate the factor for the given precision
|
|
48
|
+
const factor = Math.pow(10, precision);
|
|
49
|
+
const stepSize = Math.pow(10, -precision);
|
|
50
|
+
const halfStep = stepSize / 2;
|
|
51
|
+
|
|
52
|
+
// Check if the original value is an integer
|
|
53
|
+
const isInteger = Math.floor(value) === value;
|
|
54
|
+
|
|
55
|
+
// For integers with decimal precision, the boundary is 0.5 less than the value
|
|
56
|
+
if (isInteger && precision > 0) {
|
|
57
|
+
return value - 0.5;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Truncate the value to the specified precision using floor (always toward negative infinity)
|
|
61
|
+
const truncated = Math.floor(value * factor) / factor;
|
|
62
|
+
|
|
63
|
+
// The low boundary is the truncated value minus half a step
|
|
64
|
+
return truncated - halfStep;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the high boundary of a decimal value at the specified precision
|
|
69
|
+
*
|
|
70
|
+
* Returns the greatest possible value of the input to the specified precision.
|
|
71
|
+
* For the given precision, returns the ceiling value plus half the step size.
|
|
72
|
+
*
|
|
73
|
+
* @param value The decimal value
|
|
74
|
+
* @param precision The number of decimal places (optional, defaults to input precision + 1)
|
|
75
|
+
* @returns The high boundary value, or null if precision is invalid
|
|
76
|
+
*/
|
|
77
|
+
export function getDecimalHighBoundary(value: number, precision?: number): number | null {
|
|
78
|
+
// Default precision is the input's precision (for decimals) or 1 (for integers)
|
|
79
|
+
if (precision === undefined) {
|
|
80
|
+
const str = value.toString();
|
|
81
|
+
const decimalIndex = str.indexOf('.');
|
|
82
|
+
const inputPrecision = decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
|
|
83
|
+
// For integers, default to precision 1; for decimals, use their precision
|
|
84
|
+
precision = inputPrecision === 0 ? 1 : Math.min(inputPrecision, MAX_DECIMAL_PRECISION);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Invalid precision returns null (empty in FHIRPath)
|
|
88
|
+
if (precision < 0 || precision > MAX_DECIMAL_PRECISION) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// For precision 0 (integer precision)
|
|
93
|
+
if (precision === 0) {
|
|
94
|
+
// For integers at precision 0, return ceiling + 1 for positive, floor - 1 for negative
|
|
95
|
+
// 1.highBoundary(0) = 2
|
|
96
|
+
// -1.highBoundary(0) = -2
|
|
97
|
+
const ceiling = value >= 0 ? Math.ceil(value) : Math.floor(value);
|
|
98
|
+
return ceiling + 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate the factor for the given precision
|
|
102
|
+
const factor = Math.pow(10, precision);
|
|
103
|
+
const stepSize = Math.pow(10, -precision);
|
|
104
|
+
const halfStep = stepSize / 2;
|
|
105
|
+
|
|
106
|
+
// Check if the original value is an integer
|
|
107
|
+
const isInteger = Math.floor(value) === value;
|
|
108
|
+
|
|
109
|
+
// For integers with decimal precision, the boundary is 0.5 more than the value
|
|
110
|
+
if (isInteger && precision > 0) {
|
|
111
|
+
return value + 0.5;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Ceiling the value to the specified precision (always toward positive infinity)
|
|
115
|
+
const ceiling = Math.ceil(value * factor) / factor;
|
|
116
|
+
|
|
117
|
+
// The high boundary is the ceiling value plus half a step
|
|
118
|
+
return ceiling + halfStep;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format a decimal value to a specific number of decimal places
|
|
123
|
+
* Ensures trailing zeros are preserved in the output string
|
|
124
|
+
*/
|
|
125
|
+
export function formatDecimalWithPrecision(value: number, precision: number): string {
|
|
126
|
+
// Handle special case for precision 0
|
|
127
|
+
if (precision === 0) {
|
|
128
|
+
return String(Math.round(value));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Use toFixed to ensure the right number of decimal places
|
|
132
|
+
return value.toFixed(precision);
|
|
133
|
+
}
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import type { FunctionDefinition, LiteralNode } from '../types';
|
|
1
|
+
import type { FunctionDefinition, LiteralNode, AnalysisContext, InternalAnalysisResult } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { RuntimeContextManager } from '../interpreter';
|
|
3
|
+
import { RuntimeContextManager } from '../interpreter/runtime-context';
|
|
4
4
|
import { type FunctionEvaluator } from '../types';
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
5
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
6
|
+
import { DiagnosticSeverity } from '../types';
|
|
7
|
+
import { toDiagnostic } from '../errors';
|
|
6
8
|
|
|
7
9
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
8
10
|
if (args.length < 1) {
|
|
9
11
|
throw Errors.invalidOperation('defineVariable requires at least 1 argument');
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
let varName: string;
|
|
15
|
+
|
|
16
|
+
// Check if first argument is a literal
|
|
17
|
+
const nameArg = args[0];
|
|
18
|
+
if (nameArg && nameArg.type === 'Literal' && (nameArg as LiteralNode).valueType === 'string') {
|
|
19
|
+
// Fast path: literal string
|
|
20
|
+
varName = (nameArg as LiteralNode).value as string;
|
|
21
|
+
} else {
|
|
22
|
+
// Slow path: evaluate expression to get name
|
|
23
|
+
const nameResult = await evaluator(nameArg!, input, context);
|
|
24
|
+
if (nameResult.value.length === 0) {
|
|
25
|
+
throw Errors.invalidOperation('Variable name expression evaluated to empty');
|
|
26
|
+
}
|
|
27
|
+
const firstValue = nameResult.value[0];
|
|
28
|
+
if (!firstValue) {
|
|
29
|
+
throw Errors.invalidOperation('Variable name expression evaluated to empty');
|
|
30
|
+
}
|
|
31
|
+
const nameValue = unbox(firstValue);
|
|
32
|
+
if (typeof nameValue !== 'string') {
|
|
33
|
+
throw Errors.invalidOperation('Variable name must evaluate to a string');
|
|
34
|
+
}
|
|
35
|
+
varName = nameValue;
|
|
15
36
|
}
|
|
16
|
-
|
|
17
|
-
const varName = nameNode.value as string;
|
|
18
37
|
|
|
19
38
|
let value: any[];
|
|
20
39
|
|
|
@@ -23,29 +42,30 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
23
42
|
value = input;
|
|
24
43
|
} else {
|
|
25
44
|
// Two arguments: defineVariable(name, value) - evaluate value expression
|
|
26
|
-
|
|
45
|
+
// $this should be set to the input collection (unboxed to avoid double-boxing)
|
|
46
|
+
const unboxedInput = input.map(v => unbox(v));
|
|
47
|
+
const tempContext = RuntimeContextManager.setVariable(context, '$this', unboxedInput, true);
|
|
27
48
|
const valueExpr = args[1];
|
|
28
49
|
if (!valueExpr) {
|
|
29
50
|
throw Errors.invalidOperation('defineVariable requires a value expression');
|
|
30
51
|
}
|
|
52
|
+
|
|
31
53
|
const valueResult = await evaluator(valueExpr, input, tempContext);
|
|
32
54
|
value = valueResult.value;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
// Set the variable using RuntimeContextManager (handles prefixes and checks)
|
|
58
|
+
// This will throw an error if the variable is already defined (per spec)
|
|
36
59
|
const newContext = RuntimeContextManager.setVariable(context, varName, value);
|
|
37
|
-
|
|
38
|
-
// If newContext is same as context, variable already existed - return empty
|
|
39
|
-
if (newContext === context) {
|
|
40
|
-
return { value: [], context };
|
|
41
|
-
}
|
|
42
60
|
|
|
61
|
+
|
|
43
62
|
// Pass through input unchanged
|
|
44
63
|
return { value: input, context: newContext };
|
|
45
64
|
};
|
|
46
65
|
|
|
47
66
|
export const defineVariableFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
48
67
|
name: 'defineVariable',
|
|
68
|
+
doesNotPropagateEmpty: true, // defineVariable should always execute to set the variable
|
|
49
69
|
category: ['context'],
|
|
50
70
|
description: 'Defines a variable in the evaluation context',
|
|
51
71
|
examples: [
|
|
@@ -58,9 +78,102 @@ export const defineVariableFunction: FunctionDefinition & { evaluate: FunctionEv
|
|
|
58
78
|
input: { type: 'Any', singleton: false },
|
|
59
79
|
parameters: [
|
|
60
80
|
{ name: 'name', type: { type: 'String', singleton: true } },
|
|
61
|
-
{ name: 'value', type: { type: 'Any', singleton: false }, optional: true },
|
|
81
|
+
{ name: 'value', type: { type: 'Any', singleton: false }, optional: true, expression: true },
|
|
62
82
|
],
|
|
63
83
|
result: { type: 'Any', singleton: false },
|
|
64
84
|
}],
|
|
65
|
-
evaluate
|
|
66
|
-
|
|
85
|
+
evaluate,
|
|
86
|
+
async inferResultType(analyzer, node, inputType) {
|
|
87
|
+
// defineVariable returns its input type unchanged
|
|
88
|
+
return inputType || { type: 'Any', singleton: false };
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Analysis-time behavior for defineVariable.
|
|
92
|
+
* Adds the variable to the context for downstream expressions.
|
|
93
|
+
*/
|
|
94
|
+
async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
|
|
95
|
+
const diagnostics: any[] = [];
|
|
96
|
+
|
|
97
|
+
// Validate we have at least one argument
|
|
98
|
+
if (args.length < 1) {
|
|
99
|
+
return {
|
|
100
|
+
type: context.inputType,
|
|
101
|
+
diagnostics: [{
|
|
102
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
103
|
+
message: 'defineVariable requires at least 1 argument',
|
|
104
|
+
severity: DiagnosticSeverity.Error
|
|
105
|
+
}]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// First argument: variable name (can be literal or expression)
|
|
110
|
+
const nameNode = args[0];
|
|
111
|
+
let varName: string | undefined;
|
|
112
|
+
let isDynamic = false;
|
|
113
|
+
|
|
114
|
+
if (nameNode && nameNode.type === 'Literal' && (nameNode as LiteralNode).valueType === 'string') {
|
|
115
|
+
// Static variable name - full analysis
|
|
116
|
+
varName = (nameNode as LiteralNode).value as string;
|
|
117
|
+
|
|
118
|
+
// Check if variable already exists
|
|
119
|
+
if (context.userVariables.has(varName)) {
|
|
120
|
+
// Flag as warning in analysis; runtime enforces error with proper code (FP6009)
|
|
121
|
+
diagnostics.push({
|
|
122
|
+
range: nameNode.range,
|
|
123
|
+
message: `Variable '${varName}' is already defined`,
|
|
124
|
+
severity: DiagnosticSeverity.Warning
|
|
125
|
+
});
|
|
126
|
+
return { type: context.inputType, diagnostics, context };
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Dynamic variable name - limited analysis
|
|
130
|
+
isDynamic = true;
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
range: nameNode?.range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
133
|
+
message: 'Dynamic variable name: cannot validate variable references at compile time',
|
|
134
|
+
severity: DiagnosticSeverity.Warning
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Still analyze the expression for other errors
|
|
138
|
+
if (nameNode) {
|
|
139
|
+
const nameResult = await context.analyzeNode(nameNode);
|
|
140
|
+
diagnostics.push(...nameResult.diagnostics);
|
|
141
|
+
|
|
142
|
+
// Check if it evaluates to string type
|
|
143
|
+
if (nameResult.type.type !== 'String') {
|
|
144
|
+
diagnostics.push({
|
|
145
|
+
range: nameNode.range,
|
|
146
|
+
message: 'Variable name expression must evaluate to String type',
|
|
147
|
+
severity: DiagnosticSeverity.Error
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Determine the type of the variable
|
|
154
|
+
let varType = context.inputType;
|
|
155
|
+
|
|
156
|
+
if (args.length >= 2 && args[1]) {
|
|
157
|
+
// If value expression provided, analyze it to get its type
|
|
158
|
+
const valueResult = await context.analyzeNode(args[1]);
|
|
159
|
+
diagnostics.push(...valueResult.diagnostics);
|
|
160
|
+
varType = valueResult.type;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Return with modified context
|
|
164
|
+
// For static names, add the variable to context
|
|
165
|
+
// For dynamic names, mark that dynamic variables exist
|
|
166
|
+
let resultContext = context;
|
|
167
|
+
if (varName) {
|
|
168
|
+
resultContext = context.withUserVariable(varName, varType);
|
|
169
|
+
} else if (isDynamic) {
|
|
170
|
+
resultContext = context.withDynamicVariables();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
type: context.inputType, // defineVariable returns input unchanged
|
|
175
|
+
diagnostics,
|
|
176
|
+
context: resultContext
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition } from '../types';
|
|
2
2
|
import type { FunctionEvaluator } from '../types';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// Use Set to track unique values based on unboxed values
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
7
|
if (left.length === 0 || right.length === 0) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { divideQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { divideQuantities } from '../complex-types/quantity-value';
|
|
5
|
+
import type { QuantityValue } from '../complex-types/quantity-value';
|
|
6
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
7
|
+
import { Errors } from '../errors';
|
|
7
8
|
|
|
8
9
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
9
10
|
if (left.length === 0 || right.length === 0) {
|
|
@@ -20,14 +21,18 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
20
21
|
// Check if both are quantities
|
|
21
22
|
if (l && typeof l === 'object' && 'unit' in l &&
|
|
22
23
|
r && typeof r === 'object' && 'unit' in r) {
|
|
23
|
-
const
|
|
24
|
+
const rightQuantity = r as QuantityValue;
|
|
25
|
+
if (rightQuantity.value === 0) {
|
|
26
|
+
throw Errors.divisionByZero();
|
|
27
|
+
}
|
|
28
|
+
const result = divideQuantities(l as QuantityValue, rightQuantity);
|
|
24
29
|
return { value: result ? [box(result, { type: 'Quantity', singleton: true })] : [], context };
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
// Handle quantity / number
|
|
28
33
|
if (l && typeof l === 'object' && 'unit' in l && typeof r === 'number') {
|
|
29
34
|
if (r === 0) {
|
|
30
|
-
|
|
35
|
+
throw Errors.divisionByZero();
|
|
31
36
|
}
|
|
32
37
|
const q = l as QuantityValue;
|
|
33
38
|
return { value: [box({ value: q.value / r, unit: q.unit }, { type: 'Quantity', singleton: true })], context };
|
|
@@ -36,7 +41,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
36
41
|
// Handle numeric division
|
|
37
42
|
if (typeof l === 'number' && typeof r === 'number') {
|
|
38
43
|
if (r === 0) {
|
|
39
|
-
|
|
44
|
+
throw Errors.divisionByZero();
|
|
40
45
|
}
|
|
41
46
|
return { value: [box(l / r, { type: 'Any', singleton: true })], context };
|
|
42
47
|
}
|
|
@@ -51,7 +56,7 @@ export const divideOperator: OperatorDefinition & { evaluate: OperationEvaluator
|
|
|
51
56
|
category: ['arithmetic'],
|
|
52
57
|
precedence: PRECEDENCE.MULTIPLICATIVE,
|
|
53
58
|
associativity: 'left',
|
|
54
|
-
description: 'Divides the left operand by the right operand (supported for Integer, Decimal, and Quantity). The result is always Decimal, even if inputs are both Integer. Division by zero
|
|
59
|
+
description: 'Divides the left operand by the right operand (supported for Integer, Decimal, and Quantity). The result is always Decimal, even if inputs are both Integer. Division by zero throws an error.',
|
|
55
60
|
examples: ['10 / 2', '7.5 / 1.5', '12 \'cm2\' / 3 \'cm\'', '12 / 0'],
|
|
56
61
|
signatures: [
|
|
57
62
|
{
|
|
@@ -2,7 +2,7 @@ import type { OperatorDefinition } from '../types';
|
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
3
|
import { PRECEDENCE } from '../types';
|
|
4
4
|
import type { OperationEvaluator } from '../types';
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
5
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
6
6
|
|
|
7
7
|
// Note: The dot operator is special and is typically handled directly in the interpreter
|
|
8
8
|
// because it needs to evaluate its operands in sequence, not in parallel
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import type { FunctionDefinition } from
|
|
2
|
-
import type { FunctionEvaluator } from
|
|
3
|
-
import { box, unbox } from
|
|
1
|
+
import type { FunctionDefinition } from "../types";
|
|
2
|
+
import type { FunctionEvaluator } from "../types";
|
|
3
|
+
import { box, unbox } from "../interpreter/boxing";
|
|
4
4
|
|
|
5
|
-
export const evaluate: FunctionEvaluator = async (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const evaluate: FunctionEvaluator = async (
|
|
6
|
+
input,
|
|
7
|
+
context,
|
|
8
|
+
args,
|
|
9
|
+
evaluator,
|
|
10
|
+
) => {
|
|
11
|
+
return {
|
|
12
|
+
value: [box(input.length === 0, { type: "Boolean", singleton: true })],
|
|
13
|
+
context,
|
|
9
14
|
};
|
|
10
15
|
};
|
|
11
16
|
|
|
12
|
-
export const emptyFunction: FunctionDefinition & {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
17
|
+
export const emptyFunction: FunctionDefinition & {
|
|
18
|
+
evaluate: FunctionEvaluator;
|
|
19
|
+
} = {
|
|
20
|
+
name: "empty",
|
|
21
|
+
doesNotPropagateEmpty: true,
|
|
22
|
+
category: ["collection", "logical"],
|
|
23
|
+
description: "Returns true if the collection is empty",
|
|
24
|
+
examples: ["Patient.name.empty()"],
|
|
25
|
+
signatures: [
|
|
26
|
+
{
|
|
27
|
+
name: "empty",
|
|
28
|
+
input: { type: "Any", singleton: false },
|
|
29
|
+
parameters: [],
|
|
30
|
+
result: { type: "Boolean", singleton: true },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
evaluate,
|
|
34
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// Validate arguments
|
|
@@ -36,6 +36,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
36
36
|
}
|
|
37
37
|
const suffixResult = await evaluator(args[0], input, context);
|
|
38
38
|
|
|
39
|
+
// If suffix is empty, propagate empty
|
|
40
|
+
if (suffixResult.value.length === 0) {
|
|
41
|
+
return { value: [], context };
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
// Validate that suffix is a singleton string
|
|
40
45
|
if (suffixResult.value.length !== 1) {
|
|
41
46
|
throw Errors.invalidOperation('endsWith suffix argument must be a single value');
|
|
@@ -1,36 +1,19 @@
|
|
|
1
|
-
import type { OperatorDefinition } from '../types';
|
|
1
|
+
import type { OperatorDefinition, OperationEvaluator } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box } from '../interpreter/boxing';
|
|
4
|
+
import { collectionsEqual } from './comparison';
|
|
6
5
|
|
|
7
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
7
|
+
// Use the unified comparison system
|
|
8
|
+
const result = collectionsEqual(left, right);
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!boxedL || !boxedR) {
|
|
10
|
+
// null means incomparable (returns empty)
|
|
11
|
+
if (result === null) {
|
|
16
12
|
return { value: [], context };
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Check if both are quantities
|
|
23
|
-
if (l && typeof l === 'object' && 'unit' in l &&
|
|
24
|
-
r && typeof r === 'object' && 'unit' in r) {
|
|
25
|
-
const comparison = compareQuantities(l as QuantityValue, r as QuantityValue);
|
|
26
|
-
// If quantities are incomparable (different dimensions), return empty
|
|
27
|
-
if (comparison === null) {
|
|
28
|
-
return { value: [], context };
|
|
29
|
-
}
|
|
30
|
-
return { value: [box(comparison === 0, { type: 'Boolean', singleton: true })], context };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { value: [box(l === r, { type: 'Boolean', singleton: true })], context };
|
|
15
|
+
// Return the boolean result
|
|
16
|
+
return { value: [box(result, { type: 'Boolean', singleton: true })], context };
|
|
34
17
|
};
|
|
35
18
|
|
|
36
19
|
export const equalOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -41,11 +24,19 @@ export const equalOperator: OperatorDefinition & { evaluate: OperationEvaluator
|
|
|
41
24
|
associativity: 'left',
|
|
42
25
|
description: 'Returns true if the left collection is equal to the right collection. For single items, comparison is type-specific. For collections, comparison is order-dependent.',
|
|
43
26
|
examples: ['name = "John"', 'Patient.name.given = "John"', '5 = 5', '@2018-03-01 = @2018-03-01'],
|
|
44
|
-
signatures: [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
27
|
+
signatures: [
|
|
28
|
+
{
|
|
29
|
+
name: 'equal',
|
|
30
|
+
left: { type: 'Any', singleton: true },
|
|
31
|
+
right: { type: 'Any', singleton: true },
|
|
32
|
+
result: { type: 'Boolean', singleton: true },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'equal',
|
|
36
|
+
left: { type: 'Any', singleton: false },
|
|
37
|
+
right: { type: 'Any', singleton: false },
|
|
38
|
+
result: { type: 'Boolean', singleton: true },
|
|
39
|
+
}
|
|
40
|
+
],
|
|
50
41
|
evaluate
|
|
51
42
|
};
|