@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.2fee5d9.20250808110507

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 (101) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +195 -77
  3. package/dist/index.js +2541 -981
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/analyzer.ts +536 -64
  7. package/src/boxing.ts +124 -0
  8. package/src/errors.ts +246 -0
  9. package/src/index.ts +57 -9
  10. package/src/inspect.ts +307 -174
  11. package/src/interpreter.ts +299 -48
  12. package/src/model-provider.ts +110 -8
  13. package/src/operations/abs-function.ts +19 -10
  14. package/src/operations/aggregate-function.ts +11 -3
  15. package/src/operations/all-function.ts +18 -8
  16. package/src/operations/allFalse-function.ts +9 -6
  17. package/src/operations/allTrue-function.ts +9 -6
  18. package/src/operations/and-operator.ts +20 -11
  19. package/src/operations/anyFalse-function.ts +6 -4
  20. package/src/operations/anyTrue-function.ts +6 -4
  21. package/src/operations/as-operator.ts +1 -0
  22. package/src/operations/ceiling-function.ts +9 -5
  23. package/src/operations/children-function.ts +94 -0
  24. package/src/operations/combine-function.ts +4 -2
  25. package/src/operations/combine-operator.ts +18 -4
  26. package/src/operations/contains-function.ts +20 -8
  27. package/src/operations/contains-operator.ts +14 -6
  28. package/src/operations/count-function.ts +2 -1
  29. package/src/operations/defineVariable-function.ts +5 -3
  30. package/src/operations/descendants-function.ts +62 -0
  31. package/src/operations/distinct-function.ts +16 -4
  32. package/src/operations/div-operator.ts +15 -2
  33. package/src/operations/divide-operator.ts +10 -5
  34. package/src/operations/dot-operator.ts +3 -1
  35. package/src/operations/empty-function.ts +2 -1
  36. package/src/operations/endsWith-function.ts +22 -10
  37. package/src/operations/equal-operator.ts +18 -6
  38. package/src/operations/equivalent-operator.ts +15 -10
  39. package/src/operations/exclude-function.ts +12 -10
  40. package/src/operations/exists-function.ts +14 -7
  41. package/src/operations/first-function.ts +7 -4
  42. package/src/operations/floor-function.ts +9 -5
  43. package/src/operations/greater-operator.ts +9 -4
  44. package/src/operations/greater-or-equal-operator.ts +9 -4
  45. package/src/operations/iif-function.ts +18 -16
  46. package/src/operations/implies-operator.ts +22 -7
  47. package/src/operations/in-operator.ts +17 -7
  48. package/src/operations/index.ts +3 -0
  49. package/src/operations/indexOf-function.ts +22 -10
  50. package/src/operations/intersect-function.ts +17 -20
  51. package/src/operations/is-operator.ts +63 -14
  52. package/src/operations/isDistinct-function.ts +12 -6
  53. package/src/operations/join-function.ts +15 -4
  54. package/src/operations/last-function.ts +7 -4
  55. package/src/operations/length-function.ts +12 -5
  56. package/src/operations/less-operator.ts +9 -4
  57. package/src/operations/less-or-equal-operator.ts +9 -4
  58. package/src/operations/less-than.ts +25 -1
  59. package/src/operations/lower-function.ts +12 -5
  60. package/src/operations/minus-operator.ts +9 -4
  61. package/src/operations/mod-operator.ts +19 -2
  62. package/src/operations/multiply-operator.ts +11 -6
  63. package/src/operations/not-equal-operator.ts +15 -6
  64. package/src/operations/not-equivalent-operator.ts +15 -10
  65. package/src/operations/not-function.ts +12 -4
  66. package/src/operations/ofType-function.ts +135 -0
  67. package/src/operations/or-operator.ts +20 -11
  68. package/src/operations/plus-operator.ts +18 -6
  69. package/src/operations/power-function.ts +21 -9
  70. package/src/operations/replace-function.ts +26 -9
  71. package/src/operations/round-function.ts +23 -8
  72. package/src/operations/select-function.ts +12 -6
  73. package/src/operations/single-function.ts +5 -3
  74. package/src/operations/skip-function.ts +12 -5
  75. package/src/operations/split-function.ts +24 -9
  76. package/src/operations/sqrt-function.ts +9 -5
  77. package/src/operations/startsWith-function.ts +20 -8
  78. package/src/operations/subsetOf-function.ts +14 -11
  79. package/src/operations/substring-function.ts +36 -19
  80. package/src/operations/supersetOf-function.ts +14 -11
  81. package/src/operations/tail-function.ts +3 -1
  82. package/src/operations/take-function.ts +12 -5
  83. package/src/operations/toBoolean-function.ts +18 -11
  84. package/src/operations/toDecimal-function.ts +13 -6
  85. package/src/operations/toInteger-function.ts +13 -6
  86. package/src/operations/toString-function.ts +17 -10
  87. package/src/operations/trace-function.ts +12 -5
  88. package/src/operations/trim-function.ts +11 -4
  89. package/src/operations/truncate-function.ts +9 -5
  90. package/src/operations/unary-minus-operator.ts +22 -12
  91. package/src/operations/unary-plus-operator.ts +1 -0
  92. package/src/operations/union-function.ts +19 -24
  93. package/src/operations/union-operator.ts +1 -0
  94. package/src/operations/upper-function.ts +12 -5
  95. package/src/operations/where-function.ts +15 -8
  96. package/src/operations/xor-operator.ts +8 -3
  97. package/src/parser.ts +391 -8
  98. package/src/quantity-value.ts +4 -8
  99. package/src/registry.ts +3 -3
  100. package/src/types.ts +10 -6
  101. package/src/parser-base.ts +0 -400
package/src/inspect.ts CHANGED
@@ -1,204 +1,337 @@
1
- import { Parser } from './parser';
1
+ import type { TypeInfo, ASTNode, Diagnostic } from './types';
2
+ import type { FHIRPathValue } from './boxing';
3
+ import { NodeType, DiagnosticSeverity } from './types';
4
+ import { parse } from './parser';
5
+ import { Analyzer } from './analyzer';
2
6
  import { Interpreter, RuntimeContextManager } from './interpreter';
3
- import type { ASTNode, RuntimeContext, EvaluationResult, FunctionEvaluator } from './types';
4
- import { Registry } from './registry';
5
- import * as operations from './operations';
7
+ import type { RuntimeContext } from './types';
6
8
 
7
- export interface TraceEntry {
8
- name: string;
9
- values: any[];
9
+ export interface ASTMetadata {
10
+ complexity: number;
10
11
  depth: number;
11
- timestamp: number;
12
- projection?: any[];
13
- }
14
-
15
- export interface InspectOptions {
16
- input?: unknown;
17
- variables?: Record<string, unknown>;
18
- maxTraces?: number;
12
+ operationCount: Map<string, number>;
19
13
  }
20
14
 
21
15
  export interface InspectResult {
22
- expression: string;
23
- result: any[];
24
- ast: ASTNode;
25
- traces: TraceEntry[];
26
- executionTime: number;
27
- errors?: Array<{ type: string; message: string; location?: { line: number; column: number } }>;
28
- warnings?: Array<{ code: string; message: string }>;
16
+ result: FHIRPathValue[];
17
+
18
+ ast: {
19
+ node: ASTNode;
20
+ metadata: ASTMetadata;
21
+ };
22
+
23
+ diagnostics: {
24
+ warnings: Diagnostic[];
25
+ hints: Array<{message: string; suggestion?: string}>;
26
+ };
27
+
28
+ performance: {
29
+ parseTime: number;
30
+ analyzeTime: number;
31
+ evalTime: number;
32
+ totalTime: number;
33
+ operationTimings: Map<string, number>;
34
+ };
35
+
36
+ traces?: Array<{
37
+ label: string;
38
+ value: FHIRPathValue[];
39
+ timestamp: number;
40
+ }>;
29
41
  }
30
42
 
31
- class TraceCollector {
32
- traces: TraceEntry[] = [];
33
- maxTraces?: number;
34
- startTime: number;
35
- depth: number = 0;
43
+ export interface InspectOptions {
44
+ input?: any;
45
+ variables?: Record<string, any>;
46
+ includeTraces?: boolean;
47
+ maxDepth?: number;
48
+ }
36
49
 
37
- constructor(maxTraces?: number) {
38
- this.maxTraces = maxTraces;
39
- this.startTime = Date.now();
50
+ class PerformanceTracker {
51
+ private timings = new Map<string, number>();
52
+ private startTimes = new Map<string, number>();
53
+
54
+ start(name: string): void {
55
+ this.startTimes.set(name, performance.now());
40
56
  }
41
-
42
- addTrace(name: string, values: any[], projection?: any[]) {
43
- if (this.maxTraces && this.traces.length >= this.maxTraces) {
44
- return;
57
+
58
+ end(name: string): void {
59
+ const startTime = this.startTimes.get(name);
60
+ if (startTime !== undefined) {
61
+ const duration = performance.now() - startTime;
62
+ this.timings.set(name, duration);
63
+ this.startTimes.delete(name);
45
64
  }
46
-
47
- const timestamp = Date.now() - this.startTime;
48
- this.traces.push({
49
- name,
50
- values: [...values], // Clone to prevent mutation
51
- depth: this.depth,
52
- timestamp,
53
- projection: projection ? [...projection] : undefined
54
- });
55
65
  }
56
-
57
- getTraces(): TraceEntry[] {
58
- return [...this.traces];
66
+
67
+ get(name: string): number {
68
+ return this.timings.get(name) ?? 0;
69
+ }
70
+
71
+ getAll(): Map<string, number> {
72
+ return new Map(this.timings);
59
73
  }
60
74
  }
61
75
 
62
- function createTraceInterceptor(collector: TraceCollector): FunctionEvaluator {
63
- const originalEvaluator = operations.traceFunction.evaluate;
64
-
65
- return (input: any[], context: RuntimeContext, args: ASTNode[], evaluator: any): EvaluationResult => {
66
- // Check if we've reached max traces
67
- if (collector.maxTraces && collector.traces.length >= collector.maxTraces) {
68
- // Still call original to maintain console output
69
- return originalEvaluator(input, context, args, evaluator);
70
- }
71
-
72
- // Evaluate the name argument
73
- if (args.length === 0) {
74
- collector.addTrace('(unnamed)', input);
75
- return originalEvaluator(input, context, args, evaluator);
76
- }
77
-
78
- const nameResult = evaluator(args[0], input, context);
79
- if (nameResult.value.length !== 1 || typeof nameResult.value[0] !== 'string') {
80
- return originalEvaluator(input, context, args, evaluator);
81
- }
76
+ function analyzeAST(node: ASTNode, maxDepth = 100): ASTMetadata {
77
+ const operationCount = new Map<string, number>();
78
+ let complexity = 0;
79
+ let maxDepthFound = 0;
80
+
81
+ function visit(n: ASTNode, depth: number): void {
82
+ if (depth > maxDepth) return;
83
+ maxDepthFound = Math.max(maxDepthFound, depth);
82
84
 
83
- const name = nameResult.value[0];
85
+ // Count operation types
86
+ const opKey = n.type;
87
+ operationCount.set(opKey, (operationCount.get(opKey) ?? 0) + 1);
84
88
 
85
- // If projection argument is provided, evaluate it
86
- let projection: any[] | undefined;
87
- if (args.length === 2 && args[1]) {
88
- const projectionResult = evaluator(args[1], input, context);
89
- projection = projectionResult.value;
89
+ // Calculate complexity based on node type
90
+ switch (n.type) {
91
+ case NodeType.Binary:
92
+ complexity += 2;
93
+ const binary = n as any;
94
+ visit(binary.left, depth + 1);
95
+ visit(binary.right, depth + 1);
96
+ break;
97
+
98
+ case NodeType.Function:
99
+ complexity += 3;
100
+ const func = n as any;
101
+ if (func.name) visit(func.name, depth + 1);
102
+ func.args?.forEach((arg: ASTNode) => visit(arg, depth + 1));
103
+ break;
104
+
105
+ case NodeType.Unary:
106
+ complexity += 1;
107
+ visit((n as any).operand, depth + 1);
108
+ break;
109
+
110
+ case NodeType.Index:
111
+ complexity += 2;
112
+ const index = n as any;
113
+ visit(index.expression, depth + 1);
114
+ visit(index.index, depth + 1);
115
+ break;
116
+
117
+ case NodeType.Collection:
118
+ complexity += 1;
119
+ (n as any).elements?.forEach((el: ASTNode) => visit(el, depth + 1));
120
+ break;
121
+
122
+ case NodeType.MembershipTest:
123
+ complexity += 2;
124
+ const membership = n as any;
125
+ visit(membership.expression, depth + 1);
126
+ if (membership.typeExpression) visit(membership.typeExpression, depth + 1);
127
+ break;
128
+
129
+ case NodeType.TypeCast:
130
+ complexity += 2;
131
+ const typeCast = n as any;
132
+ visit(typeCast.expression, depth + 1);
133
+ if (typeCast.typeExpression) visit(typeCast.typeExpression, depth + 1);
134
+ break;
135
+
136
+ case NodeType.Literal:
137
+ case NodeType.Identifier:
138
+ case NodeType.Variable:
139
+ case NodeType.TypeReference:
140
+ case NodeType.TypeOrIdentifier:
141
+ complexity += 1;
142
+ break;
90
143
  }
91
-
92
- collector.addTrace(name, input, projection);
93
-
94
- // Call original evaluator to maintain console output and correct behavior
95
- return originalEvaluator(input, context, args, evaluator);
144
+ }
145
+
146
+ visit(node, 0);
147
+
148
+ return {
149
+ complexity,
150
+ depth: maxDepthFound,
151
+ operationCount
96
152
  };
97
153
  }
98
154
 
155
+ function generateHints(ast: ASTNode): Array<{message: string; suggestion?: string}> {
156
+ const hints: Array<{message: string; suggestion?: string}> = [];
157
+
158
+ function visit(node: ASTNode): void {
159
+ switch (node.type) {
160
+ case NodeType.Binary:
161
+ const binary = node as any;
162
+
163
+ // Hint for chain of where() calls
164
+ if (binary.operator === '.' &&
165
+ binary.right.type === NodeType.Function &&
166
+ (binary.right as any).name?.name === 'where') {
167
+ const leftFunc = binary.left.type === NodeType.Binary &&
168
+ (binary.left as any).right?.type === NodeType.Function &&
169
+ (binary.left as any).right?.name?.name === 'where';
170
+ if (leftFunc) {
171
+ hints.push({
172
+ message: 'Multiple where() calls detected',
173
+ suggestion: 'Consider combining conditions with "and" for better performance'
174
+ });
175
+ }
176
+ }
177
+
178
+ // Hint for unnecessary empty() check before first()
179
+ if (binary.operator === '.' &&
180
+ binary.right.type === NodeType.Function &&
181
+ (binary.right as any).name?.name === 'first' &&
182
+ binary.left.type === NodeType.Binary &&
183
+ (binary.left as any).right?.type === NodeType.Function &&
184
+ (binary.left as any).right?.name?.name === 'empty') {
185
+ hints.push({
186
+ message: 'Unnecessary empty() check before first()',
187
+ suggestion: 'first() returns empty collection if input is empty'
188
+ });
189
+ }
190
+
191
+ visit(binary.left);
192
+ visit(binary.right);
193
+ break;
194
+
195
+ case NodeType.Function:
196
+ const func = node as any;
197
+
198
+ // Hint for count() > 0 pattern
199
+ if (func.name?.name === 'count' && func.parent?.type === NodeType.Binary) {
200
+ const parent = func.parent as any;
201
+ if (parent.operator === '>' &&
202
+ parent.right?.type === NodeType.Literal &&
203
+ parent.right?.value === 0) {
204
+ hints.push({
205
+ message: 'count() > 0 pattern detected',
206
+ suggestion: 'Consider using exists() for better clarity'
207
+ });
208
+ }
209
+ }
210
+
211
+ func.args?.forEach((arg: ASTNode) => visit(arg));
212
+ break;
213
+
214
+ case NodeType.Collection:
215
+ (node as any).elements?.forEach((el: ASTNode) => visit(el));
216
+ break;
217
+
218
+ case NodeType.Unary:
219
+ visit((node as any).operand);
220
+ break;
221
+
222
+ case NodeType.Index:
223
+ const index = node as any;
224
+ visit(index.expression);
225
+ visit(index.index);
226
+ break;
227
+
228
+ case NodeType.MembershipTest:
229
+ const membership = node as any;
230
+ visit(membership.expression);
231
+ if (membership.typeExpression) visit(membership.typeExpression);
232
+ break;
233
+
234
+ case NodeType.TypeCast:
235
+ const typeCast = node as any;
236
+ visit(typeCast.expression);
237
+ if (typeCast.typeExpression) visit(typeCast.typeExpression);
238
+ break;
239
+ }
240
+ }
241
+
242
+ visit(ast);
243
+ return hints;
244
+ }
245
+
99
246
  export function inspect(
100
- expression: string,
247
+ expression: string,
101
248
  options: InspectOptions = {}
102
249
  ): InspectResult {
103
- const startTime = Date.now();
250
+ const tracker = new PerformanceTracker();
251
+ const traces: InspectResult['traces'] = [];
104
252
 
105
- try {
106
- const parser = new Parser(expression);
107
- const parseResult = parser.parse();
108
-
109
- // Collect parse errors
110
- const errors = parseResult.errors.map(err => ({
111
- type: 'ParseError',
112
- message: err.message,
113
- location: err.position ? {
114
- line: err.position.line,
115
- column: err.position.offset || 0
116
- } : undefined
117
- }));
118
-
119
- if (errors.length > 0) {
120
- return {
121
- expression,
122
- result: [],
123
- ast: parseResult.ast,
124
- traces: [],
125
- executionTime: Date.now() - startTime,
126
- errors
127
- };
253
+ tracker.start('total');
254
+
255
+ // Parse
256
+ tracker.start('parse');
257
+ const parseResult = parse(expression);
258
+ const ast = parseResult.ast;
259
+ tracker.end('parse');
260
+
261
+ // Analyze
262
+ tracker.start('analyze');
263
+ const analyzer = new Analyzer();
264
+ const analysisResult = analyzer.analyze(ast, options.variables);
265
+ const warnings = analysisResult.diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning);
266
+ const hints = generateHints(ast);
267
+ tracker.end('analyze');
268
+
269
+ // Analyze AST metadata
270
+ const metadata = analyzeAST(ast, options.maxDepth);
271
+
272
+ // Setup context for evaluation
273
+ const input = options.input === undefined ? [] : Array.isArray(options.input) ? options.input : [options.input];
274
+ let context = RuntimeContextManager.create(input);
275
+ context = RuntimeContextManager.setVariable(context, '$this', input);
276
+
277
+ if (options.variables) {
278
+ for (const [key, value] of Object.entries(options.variables)) {
279
+ const varValue = Array.isArray(value) ? value : [value];
280
+ context = RuntimeContextManager.setVariable(context, key, varValue);
128
281
  }
282
+ }
283
+
284
+ // Setup trace collection if requested
285
+ if (options.includeTraces) {
286
+ // We'll collect traces by intercepting the trace function during evaluation
287
+ // This will be implemented when trace() is called during evaluation
288
+ }
289
+
290
+ // Evaluate
291
+ tracker.start('eval');
292
+ const interpreter = new Interpreter();
293
+
294
+ // Track operation timings
295
+ const operationTimings = new Map<string, number>();
296
+ const originalEvaluate = interpreter.evaluate.bind(interpreter);
297
+ interpreter.evaluate = function(node: ASTNode, inputData: any[], ctx: RuntimeContext) {
298
+ const opStart = performance.now();
299
+ const result = originalEvaluate(node, inputData, ctx);
300
+ const opTime = performance.now() - opStart;
129
301
 
130
- const ast = parseResult.ast;
131
-
132
- // Create trace collector
133
- const traceCollector = new TraceCollector(options.maxTraces);
134
-
135
- // Create a custom registry with our trace interceptor
136
- const registry = new Registry();
137
-
138
- // Store original trace function
139
- const originalTraceEvaluator = operations.traceFunction.evaluate;
140
-
141
- // Replace with interceptor
142
- (operations.traceFunction as any).evaluate = createTraceInterceptor(traceCollector);
302
+ const opKey = node.type === NodeType.Function ?
303
+ `Function:${(node as any).name?.name || 'unknown'}` :
304
+ node.type === NodeType.Binary ?
305
+ `Binary:${(node as any).operator}` :
306
+ node.type;
143
307
 
144
- try {
145
- // Create interpreter with our registry
146
- const interpreter = new Interpreter(registry);
147
-
148
- const input = options.input === undefined ? [] : Array.isArray(options.input) ? options.input : [options.input];
149
-
150
- // Create context with variables
151
- let context = RuntimeContextManager.create(input);
152
- context = RuntimeContextManager.setVariable(context, '$this', input);
153
-
154
- if (options.variables) {
155
- for (const [key, value] of Object.entries(options.variables)) {
156
- const varValue = Array.isArray(value) ? value : [value];
157
- context = RuntimeContextManager.setVariable(context, key, varValue);
158
- }
159
- }
160
-
161
- let result: any[] = [];
162
- let evalErrors: Array<{ type: string; message: string }> = [];
163
-
164
- try {
165
- const evalResult = interpreter.evaluate(ast, input, context);
166
- result = evalResult.value;
167
- } catch (error) {
168
- evalErrors.push({
169
- type: 'EvaluationError',
170
- message: error instanceof Error ? error.message : String(error)
171
- });
172
- }
173
-
174
- const executionTime = Date.now() - startTime;
175
-
176
- return {
177
- expression,
178
- result,
179
- ast,
180
- traces: traceCollector.getTraces(),
181
- executionTime,
182
- errors: evalErrors.length > 0 ? evalErrors : undefined
183
- };
184
-
185
- } finally {
186
- // Always restore original evaluator
187
- (operations.traceFunction as any).evaluate = originalTraceEvaluator;
188
- }
308
+ operationTimings.set(opKey, (operationTimings.get(opKey) ?? 0) + opTime);
189
309
 
190
- } catch (error) {
191
- // Unexpected error
192
- return {
193
- expression,
194
- result: [],
195
- ast: { type: 'Error' as any, message: 'Failed to parse', range: { start: { line: 1, offset: 0 }, end: { line: 1, offset: 0 } } } as any,
196
- traces: [],
197
- executionTime: Date.now() - startTime,
198
- errors: [{
199
- type: 'UnexpectedError',
200
- message: error instanceof Error ? error.message : String(error)
201
- }]
202
- };
203
- }
310
+ return result;
311
+ };
312
+
313
+ const result = interpreter.evaluate(ast, input, context);
314
+ tracker.end('eval');
315
+
316
+ tracker.end('total');
317
+
318
+ return {
319
+ result: result.value,
320
+ ast: {
321
+ node: ast,
322
+ metadata
323
+ },
324
+ diagnostics: {
325
+ warnings,
326
+ hints
327
+ },
328
+ performance: {
329
+ parseTime: tracker.get('parse'),
330
+ analyzeTime: tracker.get('analyze'),
331
+ evalTime: tracker.get('eval'),
332
+ totalTime: tracker.get('total'),
333
+ operationTimings
334
+ },
335
+ ...(options.includeTraces && { traces })
336
+ };
204
337
  }