@atomic-ehr/fhirpath 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
@@ -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
  // Three-valued logic implementation
@@ -30,6 +30,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
30
30
  export const orOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
31
31
  symbol: 'or',
32
32
  name: 'or',
33
+ doesNotPropagateEmpty: true, // Three-valued logic: true or empty = true
33
34
  category: ['logical'],
34
35
  precedence: PRECEDENCE.OR,
35
36
  associativity: 'left',
@@ -1,9 +1,9 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { addQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { addQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
7
 
8
8
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
9
  if (left.length === 0 || right.length === 0) {
@@ -20,6 +20,69 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
20
20
  const l = unbox(boxedL);
21
21
  const r = unbox(boxedR);
22
22
 
23
+ // Check for temporal arithmetic using boxed type information
24
+ const leftType = boxedL?.typeInfo?.type;
25
+ const rightType = boxedR?.typeInfo?.type;
26
+
27
+ if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
28
+ // Left is temporal, right is quantity
29
+ const temporalType = leftType;
30
+ const temporal = l;
31
+ const quantity = r as QuantityValue;
32
+
33
+ // Import temporal utilities and create TimeQuantity
34
+ const { createTimeQuantity, add } = await import('../complex-types/temporal');
35
+
36
+ // Calendar duration units (allowed for temporal arithmetic)
37
+ const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
38
+ 'day', 'days', 'hour', 'hours', 'minute', 'minutes',
39
+ 'second', 'seconds', 'millisecond', 'milliseconds'];
40
+
41
+ // Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
42
+ const variableDurationUnits = ['a', 'mo'];
43
+
44
+ // Fixed duration UCUM units (allowed - they map directly to calendar units)
45
+ const fixedDurationUnitMap: Record<string, string> = {
46
+ 'd': 'day',
47
+ 'wk': 'week',
48
+ 'h': 'hour',
49
+ 'min': 'minute',
50
+ 's': 'second',
51
+ 'ms': 'millisecond'
52
+ };
53
+
54
+ // Check if this is a variable duration unit (not allowed)
55
+ if (variableDurationUnits.includes(quantity.unit)) {
56
+ // Variable duration units like 'a' and 'mo' cannot be added to temporal values
57
+ // because they don't have fixed durations
58
+ const { Errors } = await import('../errors');
59
+ throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
60
+ }
61
+
62
+ // Map fixed duration UCUM units to calendar units
63
+ let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
64
+
65
+ // Check if this is a valid calendar duration unit (after mapping)
66
+ if (!calendarUnits.includes(mappedUnit)) {
67
+ // Non-time units with temporal values return empty per FHIRPath spec
68
+ return { value: [], context };
69
+ }
70
+
71
+ const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
72
+
73
+ // Use the functional add operation
74
+ const result = add(temporal as any, timeQuantity);
75
+
76
+ if (temporalType === 'Date') {
77
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
78
+ } else if (temporalType === 'DateTime') {
79
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
80
+ } else if (temporalType === 'Time') {
81
+ // Let the error propagate - adding calendar units to Time should throw
82
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
83
+ }
84
+ }
85
+
23
86
  // Check if both are quantities
24
87
  if (l && typeof l === 'object' && 'unit' in l &&
25
88
  r && typeof r === 'object' && 'unit' in r) {
@@ -27,8 +90,9 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
27
90
  return { value: result ? [box(result, { type: 'Quantity', singleton: true })] : [], context };
28
91
  }
29
92
 
30
- if (typeof l === 'string' || typeof r === 'string') {
31
- return { value: [box(String(l) + String(r), { type: 'String', singleton: true })], context };
93
+ // String concatenation only works for string + string
94
+ if (typeof l === 'string' && typeof r === 'string') {
95
+ return { value: [box(l + r, { type: 'String', singleton: true })], context };
32
96
  }
33
97
 
34
98
  if (typeof l === 'number' && typeof r === 'number') {
@@ -39,8 +103,8 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
39
103
  return { value: [box(result, typeInfo)], context };
40
104
  }
41
105
 
42
- // For other types, convert to string
43
- return { value: [box(String(l) + String(r), { type: 'String', singleton: true })], context };
106
+ // For incompatible types, return empty per FHIRPath spec
107
+ return { value: [], context };
44
108
  };
45
109
 
46
110
  export const plusOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -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
  // power() takes exactly one argument (exponent)
@@ -75,14 +75,39 @@ export const powerFunction: FunctionDefinition & { evaluate: FunctionEvaluator }
75
75
  '2.5.power(2)',
76
76
  '(-1).power(0.5)'
77
77
  ],
78
- signatures: [{
79
-
80
- name: 'power',
81
- input: { type: 'Decimal', singleton: true },
82
- parameters: [
83
- { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
84
- ],
85
- result: { type: 'Decimal', singleton: true }
86
- }],
78
+ signatures: [
79
+ {
80
+ name: 'power-integer',
81
+ input: { type: 'Integer', singleton: true },
82
+ parameters: [
83
+ { name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
84
+ ],
85
+ result: { type: 'Integer', singleton: true }
86
+ },
87
+ {
88
+ name: 'power-decimal',
89
+ input: { type: 'Decimal', singleton: true },
90
+ parameters: [
91
+ { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
92
+ ],
93
+ result: { type: 'Decimal', singleton: true }
94
+ },
95
+ {
96
+ name: 'power-integer-decimal',
97
+ input: { type: 'Integer', singleton: true },
98
+ parameters: [
99
+ { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
100
+ ],
101
+ result: { type: 'Decimal', singleton: true }
102
+ },
103
+ {
104
+ name: 'power-decimal-integer',
105
+ input: { type: 'Decimal', singleton: true },
106
+ parameters: [
107
+ { name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
108
+ ],
109
+ result: { type: 'Decimal', singleton: true }
110
+ }
111
+ ],
87
112
  evaluate
88
113
  };
@@ -0,0 +1,169 @@
1
+ import type { FunctionDefinition, AnalysisContext, InternalAnalysisResult } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { RuntimeContextManager } from '../interpreter/runtime-context';
4
+ import { type FunctionEvaluator } from '../types';
5
+ import { unbox, box } from '../interpreter/boxing';
6
+ import { collectionsEqual } from './comparison';
7
+
8
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
9
+ // Repeat requires exactly one argument
10
+ if (args.length !== 1) {
11
+ throw Errors.wrongArgumentCount('repeat', 1, args.length);
12
+ }
13
+
14
+ const expression = args[0];
15
+ if (!expression) {
16
+ throw Errors.invalidOperation('repeat requires a projection expression');
17
+ }
18
+
19
+ // Result collection that will accumulate all unique items
20
+ const result: any[] = [];
21
+
22
+ // Track which items we've already seen to detect duplicates
23
+ const seen = new Set<any>();
24
+
25
+ // Helper to check if an item is already in the result
26
+ const isInResult = (item: any): boolean => {
27
+ for (const existing of result) {
28
+ // Use equals comparison to determine if items are the same
29
+ const equalResult = collectionsEqual([existing], [item]);
30
+ if (equalResult === true) {
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ };
36
+
37
+ // Initial evaluation on input collection
38
+ const initialResults: any[] = [];
39
+ for (let i = 0; i < input.length; i++) {
40
+ const boxedItem = input[i];
41
+ if (!boxedItem) continue;
42
+
43
+ const item = unbox(boxedItem);
44
+
45
+ // Create iterator context with $this and $index
46
+ let tempContext = RuntimeContextManager.withIterator(context, item, i);
47
+ tempContext = RuntimeContextManager.setVariable(tempContext, '$total', input.length);
48
+
49
+ // Evaluate expression with temporary context
50
+ const exprResult = await evaluator(expression, [boxedItem], tempContext);
51
+
52
+ // Add results from initial evaluation
53
+ for (const newItem of exprResult.value) {
54
+ if (newItem && !isInResult(newItem)) {
55
+ result.push(newItem);
56
+ initialResults.push(newItem);
57
+ }
58
+ }
59
+ }
60
+
61
+ // Now process the queue with items from the initial results
62
+ let queue = [...initialResults];
63
+
64
+ // Process items until queue is empty
65
+ while (queue.length > 0) {
66
+ const nextQueue: any[] = [];
67
+
68
+ for (let i = 0; i < queue.length; i++) {
69
+ const boxedItem = queue[i];
70
+ if (!boxedItem) continue;
71
+
72
+ const item = unbox(boxedItem);
73
+
74
+ // Create iterator context with $this and $index
75
+ let tempContext = RuntimeContextManager.withIterator(context, item, i);
76
+ tempContext = RuntimeContextManager.setVariable(tempContext, '$total', queue.length);
77
+
78
+ // Evaluate expression with temporary context
79
+ const exprResult = await evaluator(expression, [boxedItem], tempContext);
80
+
81
+ // Add new items to the result and next queue
82
+ for (const newItem of exprResult.value) {
83
+ if (newItem && !isInResult(newItem)) {
84
+ result.push(newItem);
85
+ nextQueue.push(newItem);
86
+ }
87
+ }
88
+ }
89
+
90
+ // Move to next iteration
91
+ queue = nextQueue;
92
+ }
93
+
94
+ return { value: result, context };
95
+ };
96
+
97
+ export const repeatFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
98
+ name: 'repeat',
99
+ category: ['collection'],
100
+ description: 'A version of select that will repeat the projection and add items to the output collection only if they are not already in the output collection as determined by the equals (=) operator. Can be used to traverse a tree by repeatedly selecting specific children.',
101
+ examples: [
102
+ 'ValueSet.expansion.repeat(contains)',
103
+ 'Questionnaire.repeat(item)',
104
+ 'Bundle.entry.repeat(resource.link)'
105
+ ],
106
+ signatures: [{
107
+ name: 'repeat',
108
+ input: { type: 'Any', singleton: false },
109
+ parameters: [
110
+ { name: 'projection', type: { type: 'Any', singleton: false }, expression: true },
111
+ ],
112
+ result: 'parameterType' as any,
113
+ }],
114
+ evaluate,
115
+
116
+ /**
117
+ * Analysis-time behavior for repeat.
118
+ * Similar to select, but with repeated application.
119
+ */
120
+ async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
121
+ const diagnostics: any[] = [];
122
+
123
+ if (args.length !== 1) {
124
+ return {
125
+ type: { type: 'Any', singleton: false },
126
+ diagnostics: [{
127
+ message: 'repeat expects exactly 1 argument',
128
+ severity: 'error' as any,
129
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
130
+ }]
131
+ };
132
+ }
133
+
134
+ // Get element type from input
135
+ const elementType = context.inputType.singleton
136
+ ? context.inputType
137
+ : { ...context.inputType, singleton: true };
138
+
139
+ // Create context for analyzing the projection
140
+ const projectionContext = context
141
+ .withInputType(elementType)
142
+ .withSystemVariable('$this', elementType)
143
+ .withSystemVariable('$index', { type: 'Integer', singleton: true })
144
+ .withSystemVariable('$total', { type: 'Integer', singleton: true });
145
+
146
+ // Analyze the projection expression
147
+ const projectionArg = args[0];
148
+ if (!projectionArg) {
149
+ return {
150
+ type: { type: 'Any', singleton: false },
151
+ diagnostics,
152
+ context
153
+ };
154
+ }
155
+ const projectionResult = await projectionContext.analyzeNode(projectionArg);
156
+ diagnostics.push(...projectionResult.diagnostics);
157
+
158
+ // Result type is the type returned by the projection (as a collection)
159
+ const resultType = projectionResult.type.singleton
160
+ ? { ...projectionResult.type, singleton: false }
161
+ : projectionResult.type;
162
+
163
+ return {
164
+ type: resultType,
165
+ diagnostics,
166
+ context
167
+ };
168
+ }
169
+ };
@@ -1,7 +1,7 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
3
  import type { FunctionEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
7
  // Check if we have exactly 2 arguments
@@ -0,0 +1,120 @@
1
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+
5
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
+ // Check single item in input
7
+ if (input.length === 0) {
8
+ return { value: [], context };
9
+ }
10
+
11
+ if (input.length > 1) {
12
+ throw Errors.stringSingletonRequired('replaceMatches', input.length);
13
+ }
14
+
15
+ const boxedInputValue = input[0];
16
+ if (!boxedInputValue) {
17
+ return { value: [], context };
18
+ }
19
+
20
+ const inputValue = unbox(boxedInputValue);
21
+ if (typeof inputValue !== 'string') {
22
+ throw Errors.stringOperationOnNonString('replaceMatches');
23
+ }
24
+
25
+ // Check arguments
26
+ if (args.length !== 2) {
27
+ throw Errors.wrongArgumentCount('replaceMatches', 2, args.length);
28
+ }
29
+
30
+ // Evaluate regex argument
31
+ const regexArg = args[0];
32
+ if (!regexArg) {
33
+ throw Errors.argumentRequired('replaceMatches', 'regex argument');
34
+ }
35
+
36
+ const regexResult = await evaluator(regexArg, input, context);
37
+
38
+ if (regexResult.value.length === 0) {
39
+ return { value: [], context };
40
+ }
41
+
42
+ if (regexResult.value.length > 1) {
43
+ throw Errors.singletonRequired('replaceMatches regex', regexResult.value.length);
44
+ }
45
+
46
+ const boxedRegex = regexResult.value[0];
47
+ if (!boxedRegex) {
48
+ return { value: [], context };
49
+ }
50
+
51
+ const regexPattern = unbox(boxedRegex);
52
+ if (typeof regexPattern !== 'string') {
53
+ throw Errors.invalidStringOperation('replaceMatches', 'regex argument');
54
+ }
55
+
56
+ // Evaluate substitution argument
57
+ const substitutionArg = args[1];
58
+ if (!substitutionArg) {
59
+ throw Errors.argumentRequired('replaceMatches', 'substitution argument');
60
+ }
61
+
62
+ const substitutionResult = await evaluator(substitutionArg, input, context);
63
+
64
+ if (substitutionResult.value.length === 0) {
65
+ return { value: [], context };
66
+ }
67
+
68
+ if (substitutionResult.value.length > 1) {
69
+ throw Errors.singletonRequired('replaceMatches substitution', substitutionResult.value.length);
70
+ }
71
+
72
+ const boxedSubstitution = substitutionResult.value[0];
73
+ if (!boxedSubstitution) {
74
+ return { value: [], context };
75
+ }
76
+
77
+ const substitution = unbox(boxedSubstitution);
78
+ if (typeof substitution !== 'string') {
79
+ throw Errors.invalidStringOperation('replaceMatches', 'substitution argument');
80
+ }
81
+
82
+ try {
83
+ // Create regex with unicode support, single line mode (dotAll), and global flag for all matches
84
+ // Per spec: case-sensitive, single line mode, allow Unicode
85
+ const regex = new RegExp(regexPattern, 'gus');
86
+
87
+ // JavaScript's replace supports $1, $2 etc for capture groups
88
+ // The spec also mentions named groups with ${name} syntax
89
+ // JavaScript natively supports both $1 and $<name> syntax
90
+ // We need to convert ${name} to $<name> for JavaScript compatibility
91
+ const jsSubstitution = substitution.replace(/\$\{([^}]+)\}/g, '$<$1>');
92
+
93
+ const result = inputValue.replace(regex, jsSubstitution);
94
+
95
+ return { value: [box(result, { type: 'String', singleton: true })], context };
96
+ } catch (error) {
97
+ throw new Error(`Invalid regular expression in replaceMatches(): ${(error as Error).message}`);
98
+ }
99
+ };
100
+
101
+ export const replaceMatchesFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
102
+ name: 'replaceMatches',
103
+ category: ['string'],
104
+ description: 'Replaces all matches of the regex pattern with the substitution string',
105
+ examples: [
106
+ "'test string'.replaceMatches('test', 'match')",
107
+ "'11/30/1972'.replaceMatches('\\\\b(\\\\d{1,2})/(\\\\d{1,2})/(\\\\d{2,4})\\\\b', '$2-$1-$3')",
108
+ "'test test'.replaceMatches('t', 'T')"
109
+ ],
110
+ signatures: [{
111
+ name: 'replaceMatches',
112
+ input: { type: 'String', singleton: true },
113
+ parameters: [
114
+ { name: 'regex', type: { type: 'String', singleton: true } },
115
+ { name: 'substitution', type: { type: 'String', singleton: true } }
116
+ ],
117
+ result: { type: 'String', singleton: true }
118
+ }],
119
+ evaluate
120
+ };
@@ -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
  // round() takes 0 or 1 argument (precision)
@@ -0,0 +1,66 @@
1
+ // secondOf() function - Extracts second component from Time or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRTime, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const secondOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // secondOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('secondOf', 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('secondOf', 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 Time or DateTime
31
+ if (isFHIRTime(value) || isFHIRDateTime(value)) {
32
+ // Check if second component is present
33
+ if (value.second === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the second as an Integer (0-59)
38
+ return {
39
+ value: [box(value.second, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Time or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const secondOfFunction: FunctionDefinition & { evaluate: typeof secondOfEvaluator } = {
49
+ name: 'secondOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the second component of a Time or DateTime value (0-59)',
52
+ examples: [
53
+ '@T10:30:45.secondOf()',
54
+ '@2014-01-05T10:30:45.secondOf()',
55
+ 'Observation.effectiveDateTime.secondOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'secondOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: secondOfEvaluator
66
+ };
@@ -1,8 +1,8 @@
1
- import type { FunctionDefinition } from '../types';
1
+ import type { FunctionDefinition, 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 { unbox } from '../boxing';
5
+ import { unbox } from '../interpreter/boxing';
6
6
 
7
7
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
8
8
  // Select requires exactly one argument
@@ -55,5 +55,66 @@ export const selectFunction: FunctionDefinition & { evaluate: FunctionEvaluator
55
55
  ],
56
56
  result: 'parameterType' as any,
57
57
  }],
58
- evaluate
59
- };
58
+ evaluate,
59
+
60
+ /**
61
+ * Analysis-time behavior for select.
62
+ * The projection expression needs to be analyzed with system variables in scope.
63
+ */
64
+ async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
65
+ const diagnostics: any[] = [];
66
+
67
+ if (args.length !== 1) {
68
+ return {
69
+ type: { type: 'Any', singleton: false },
70
+ diagnostics: [{
71
+ message: 'select expects exactly 1 argument',
72
+ severity: 'error' as any,
73
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
74
+ }]
75
+ };
76
+ }
77
+
78
+ // For select, we need to analyze the projection expression with:
79
+ // 1. $this set to each element type
80
+ // 2. $index available as Integer
81
+ // 3. User variables from the outer context preserved
82
+
83
+ // Get element type from input
84
+ const elementType = context.inputType.singleton
85
+ ? context.inputType
86
+ : { ...context.inputType, singleton: true };
87
+
88
+ // Create context for analyzing the projection
89
+ // Add system variables but preserve user variables
90
+ // IMPORTANT: Also update input type to the element type for property navigation
91
+ const projectionContext = context
92
+ .withInputType(elementType)
93
+ .withSystemVariable('$this', elementType)
94
+ .withSystemVariable('$index', { type: 'Integer', singleton: true })
95
+ .withSystemVariable('$total', { type: 'Integer', singleton: true });
96
+
97
+ // Analyze the projection expression
98
+ const projectionArg = args[0];
99
+ if (!projectionArg) {
100
+ return {
101
+ type: { type: 'Any', singleton: false },
102
+ diagnostics,
103
+ context
104
+ };
105
+ }
106
+ const projectionResult = await projectionContext.analyzeNode(projectionArg);
107
+ diagnostics.push(...projectionResult.diagnostics);
108
+
109
+ // Result type is the type returned by the projection (as a collection)
110
+ const resultType = projectionResult.type.singleton
111
+ ? { ...projectionResult.type, singleton: false }
112
+ : projectionResult.type;
113
+
114
+ return {
115
+ type: resultType,
116
+ diagnostics,
117
+ context // Return original context - select doesn't modify outer scope
118
+ };
119
+ }
120
+ };
@@ -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
  // single takes no arguments
@@ -1,7 +1,7 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
3
  import type { FunctionEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
7
  if (args.length !== 1) {
@@ -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
  // Handle empty input collection