@atomic-ehr/fhirpath 0.0.1-canary.69eb286.20250724163205
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 +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8256 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +149 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +589 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/context.ts +181 -0
- package/src/interpreter/interpreter.ts +484 -0
- package/src/interpreter/types.ts +132 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +422 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +719 -0
- package/src/registry/operations/filtering.ts +374 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +553 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +162 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# FHIRPath Interpreter
|
|
2
|
+
|
|
3
|
+
A stream-processing based FHIRPath interpreter implementation following the mental model described in `ideas/fhirpath-mental-model-3.md`.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The interpreter follows the core principle that **everything is a processing node** with a uniform interface:
|
|
8
|
+
- **Input**: Always a collection (even single values are collections of one)
|
|
9
|
+
- **Context**: Variables and environment data flowing parallel to data
|
|
10
|
+
- **Output**: The resulting collection
|
|
11
|
+
- **New Context**: Potentially modified context
|
|
12
|
+
|
|
13
|
+
## Implementation Status
|
|
14
|
+
|
|
15
|
+
### ✅ Phase 1: Core Infrastructure
|
|
16
|
+
- `types.ts` - Core interfaces (EvaluationResult, Context, TypeInfo)
|
|
17
|
+
- `context.ts` - Context management (variables, environment)
|
|
18
|
+
- `interpreter.ts` - Base interpreter class with node dispatch
|
|
19
|
+
|
|
20
|
+
### ✅ Phase 2: Simple Nodes
|
|
21
|
+
- **Literals**: Numbers, strings, booleans, null, collections
|
|
22
|
+
- **Identifiers**: Property navigation with flattening
|
|
23
|
+
- **Variables**: $this, $index, $total, %user-variables, %context
|
|
24
|
+
- **Dot Operator**: Pipeline semantics (left output → right input)
|
|
25
|
+
|
|
26
|
+
### ✅ Phase 3: Operators
|
|
27
|
+
- **Arithmetic**: +, -, *, /, div, mod with singleton conversion
|
|
28
|
+
- **Comparison**: =, !=, <, >, <=, >= with three-valued logic
|
|
29
|
+
- **Logical**: and, or, not, xor, implies with three-valued logic
|
|
30
|
+
- **Unary**: +, -, not
|
|
31
|
+
|
|
32
|
+
### 🚧 Phase 4: Basic Functions (Next)
|
|
33
|
+
- Function dispatch mechanism
|
|
34
|
+
- Simple value functions (first, last, count, etc.)
|
|
35
|
+
- Iterator functions (where, select, exists, all)
|
|
36
|
+
|
|
37
|
+
### 📋 Phase 5: Type System (Planned)
|
|
38
|
+
- Type hierarchy and checking
|
|
39
|
+
- is/as operators
|
|
40
|
+
- Type conversions
|
|
41
|
+
|
|
42
|
+
### 📋 Phase 6: Advanced Features (Planned)
|
|
43
|
+
- Context modification (defineVariable)
|
|
44
|
+
- Conditional evaluation (iif)
|
|
45
|
+
- Collection operations (union, intersect)
|
|
46
|
+
- Index operations
|
|
47
|
+
|
|
48
|
+
## Key Design Decisions
|
|
49
|
+
|
|
50
|
+
1. **Two-Phase Evaluation**: Control flow (top-down) and data flow (bottom-up)
|
|
51
|
+
2. **Collection Semantics**: Everything is a collection, empty represents null/missing
|
|
52
|
+
3. **Context Threading**: Context flows through expressions, modified by some operations
|
|
53
|
+
4. **Three-Valued Logic**: Empty collections represent "unknown" in boolean operations
|
|
54
|
+
5. **Singleton Rules**: Automatic conversion from single-item collections when needed
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { evaluateFHIRPath } from './interpreter/interpreter';
|
|
60
|
+
import { ContextManager } from './interpreter/context';
|
|
61
|
+
|
|
62
|
+
// Simple evaluation
|
|
63
|
+
const result = evaluateFHIRPath('Patient.name.given', patient);
|
|
64
|
+
|
|
65
|
+
// With context
|
|
66
|
+
const context = ContextManager.create();
|
|
67
|
+
const ctxWithVar = ContextManager.setVariable(context, 'threshold', [10]);
|
|
68
|
+
const result2 = evaluateFHIRPath('value > %threshold', data, ctxWithVar);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Testing
|
|
72
|
+
|
|
73
|
+
Tests are organized by implementation phase in `test/interpreter.test.ts`:
|
|
74
|
+
- Phase 2: Simple nodes (literals, identifiers, variables, dot)
|
|
75
|
+
- Phase 3: Operators (arithmetic, comparison, logical)
|
|
76
|
+
- Phase 4: Functions (coming soon)
|
|
77
|
+
|
|
78
|
+
Run tests with: `bun test test/interpreter.test.ts`
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Context } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context management utilities for FHIRPath interpreter.
|
|
5
|
+
* Context flows parallel to data through expressions.
|
|
6
|
+
*
|
|
7
|
+
* Uses JavaScript prototype chain for efficient inheritance.
|
|
8
|
+
*/
|
|
9
|
+
export class ContextManager {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new empty context
|
|
12
|
+
*/
|
|
13
|
+
static create(initialInput?: any[]): Context {
|
|
14
|
+
// Create base context with null prototype to avoid Object.prototype pollution
|
|
15
|
+
const context = Object.create(null) as Context;
|
|
16
|
+
|
|
17
|
+
// Initialize with prototype-based objects
|
|
18
|
+
context.variables = Object.create(null);
|
|
19
|
+
context.env = Object.create(null);
|
|
20
|
+
|
|
21
|
+
// Set root variables
|
|
22
|
+
context.$context = initialInput;
|
|
23
|
+
context.$resource = initialInput;
|
|
24
|
+
context.$rootResource = initialInput;
|
|
25
|
+
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a child context inheriting from parent via prototype chain
|
|
31
|
+
* O(1) operation - no copying needed
|
|
32
|
+
*/
|
|
33
|
+
static copy(context: Context): Context {
|
|
34
|
+
// Create child context with parent as prototype
|
|
35
|
+
const newContext = Object.create(context) as Context;
|
|
36
|
+
|
|
37
|
+
// Create child objects that inherit from parent's objects
|
|
38
|
+
newContext.variables = Object.create(context.variables);
|
|
39
|
+
newContext.env = Object.create(context.env);
|
|
40
|
+
|
|
41
|
+
// Root variables are inherited automatically through prototype chain
|
|
42
|
+
// No need to copy them unless they change
|
|
43
|
+
|
|
44
|
+
return newContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add or update a variable in context
|
|
49
|
+
* Only sets on current context level, shadowing parent values
|
|
50
|
+
*/
|
|
51
|
+
static setVariable(context: Context, name: string, value: any[]): Context {
|
|
52
|
+
const newContext = this.copy(context);
|
|
53
|
+
newContext.variables[name] = value;
|
|
54
|
+
return newContext;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a variable value from context
|
|
59
|
+
* Uses prototype chain for lookup
|
|
60
|
+
*/
|
|
61
|
+
static getVariable(context: Context, name: string): any[] | undefined {
|
|
62
|
+
// Check user-defined variables first (prototype chain handles inheritance)
|
|
63
|
+
if (name in context.variables) {
|
|
64
|
+
return context.variables[name];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check special root variables
|
|
68
|
+
switch (name) {
|
|
69
|
+
case 'context':
|
|
70
|
+
return context.$context;
|
|
71
|
+
case 'resource':
|
|
72
|
+
return context.$resource;
|
|
73
|
+
case 'rootResource':
|
|
74
|
+
return context.$rootResource;
|
|
75
|
+
default:
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set iterator context ($this, $index) - used by where(), select(), etc.
|
|
82
|
+
*/
|
|
83
|
+
static setIteratorContext(
|
|
84
|
+
context: Context,
|
|
85
|
+
item: any,
|
|
86
|
+
index: number
|
|
87
|
+
): Context {
|
|
88
|
+
const newContext = this.copy(context);
|
|
89
|
+
// Only set changed values - prototype provides the rest
|
|
90
|
+
newContext.env.$this = [item];
|
|
91
|
+
newContext.env.$index = index;
|
|
92
|
+
return newContext;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set aggregate context ($total) - used by aggregate()
|
|
97
|
+
*/
|
|
98
|
+
static setAggregateContext(
|
|
99
|
+
context: Context,
|
|
100
|
+
total: any[]
|
|
101
|
+
): Context {
|
|
102
|
+
const newContext = this.copy(context);
|
|
103
|
+
newContext.env.$total = total;
|
|
104
|
+
return newContext;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear iterator/aggregate context - restore to original
|
|
109
|
+
*/
|
|
110
|
+
static clearEnv(context: Context): Context {
|
|
111
|
+
const newContext = this.copy(context);
|
|
112
|
+
// Create fresh env object, effectively hiding parent's env values
|
|
113
|
+
newContext.env = Object.create(null);
|
|
114
|
+
return newContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get special environment variable
|
|
119
|
+
*/
|
|
120
|
+
static getEnvVariable(context: Context, name: '$this' | '$index' | '$total'): any {
|
|
121
|
+
switch (name) {
|
|
122
|
+
case '$this':
|
|
123
|
+
return context.env.$this;
|
|
124
|
+
case '$index':
|
|
125
|
+
return context.env.$index;
|
|
126
|
+
case '$total':
|
|
127
|
+
return context.env.$total;
|
|
128
|
+
default:
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a variable exists in context (including inherited)
|
|
135
|
+
*/
|
|
136
|
+
static hasVariable(context: Context, name: string): boolean {
|
|
137
|
+
return (name in context.variables) ||
|
|
138
|
+
['context', 'resource', 'rootResource'].includes(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Debug helper - get all variables including inherited ones
|
|
143
|
+
* Traverses prototype chain to collect all variables
|
|
144
|
+
*/
|
|
145
|
+
static getAllVariables(context: Context): Record<string, any[]> {
|
|
146
|
+
const result: Record<string, any[]> = {};
|
|
147
|
+
|
|
148
|
+
// Traverse prototype chain for user variables
|
|
149
|
+
let currentVars = context.variables;
|
|
150
|
+
while (currentVars) {
|
|
151
|
+
for (const key in currentVars) {
|
|
152
|
+
if (!(key in result) && Object.prototype.hasOwnProperty.call(currentVars, key)) {
|
|
153
|
+
result[`%${key}`] = currentVars[key]!;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
currentVars = Object.getPrototypeOf(currentVars);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Root variables
|
|
160
|
+
if (context.$context) result['%context'] = context.$context;
|
|
161
|
+
if (context.$resource) result['%resource'] = context.$resource;
|
|
162
|
+
if (context.$rootResource) result['%rootResource'] = context.$rootResource;
|
|
163
|
+
|
|
164
|
+
// Environment variables (also traverse prototype chain)
|
|
165
|
+
let currentEnv = context.env;
|
|
166
|
+
while (currentEnv) {
|
|
167
|
+
if (currentEnv.$this !== undefined && !('$this' in result)) {
|
|
168
|
+
result['$this'] = currentEnv.$this;
|
|
169
|
+
}
|
|
170
|
+
if (currentEnv.$index !== undefined && !('$index' in result)) {
|
|
171
|
+
result['$index'] = [currentEnv.$index];
|
|
172
|
+
}
|
|
173
|
+
if (currentEnv.$total !== undefined && !('$total' in result)) {
|
|
174
|
+
result['$total'] = currentEnv.$total;
|
|
175
|
+
}
|
|
176
|
+
currentEnv = Object.getPrototypeOf(currentEnv);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
}
|