@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800

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 (57) hide show
  1. package/README.md +307 -0
  2. package/dist/index.d.ts +225 -0
  3. package/dist/index.js +8185 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/analyzer/analyzer.ts +486 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +148 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +152 -0
  14. package/src/api/index.ts +57 -0
  15. package/src/api/registry.ts +128 -0
  16. package/src/api/types.ts +154 -0
  17. package/src/compiler/compiler.ts +579 -0
  18. package/src/compiler/index.ts +2 -0
  19. package/src/compiler/prototype-context-adapter.ts +99 -0
  20. package/src/compiler/types.ts +23 -0
  21. package/src/index.ts +52 -0
  22. package/src/interpreter/README.md +78 -0
  23. package/src/interpreter/interpreter.ts +485 -0
  24. package/src/interpreter/types.ts +110 -0
  25. package/src/lexer/char-tables.ts +37 -0
  26. package/src/lexer/errors.ts +31 -0
  27. package/src/lexer/index.ts +5 -0
  28. package/src/lexer/lexer.ts +745 -0
  29. package/src/lexer/token.ts +104 -0
  30. package/src/parser/ast.ts +123 -0
  31. package/src/parser/index.ts +3 -0
  32. package/src/parser/parser.ts +701 -0
  33. package/src/parser/pprint.ts +169 -0
  34. package/src/registry/default-analyzers.ts +257 -0
  35. package/src/registry/default-compilers.ts +31 -0
  36. package/src/registry/index.ts +93 -0
  37. package/src/registry/operations/arithmetic.ts +506 -0
  38. package/src/registry/operations/collection.ts +425 -0
  39. package/src/registry/operations/comparison.ts +432 -0
  40. package/src/registry/operations/existence.ts +703 -0
  41. package/src/registry/operations/filtering.ts +358 -0
  42. package/src/registry/operations/literals.ts +341 -0
  43. package/src/registry/operations/logical.ts +402 -0
  44. package/src/registry/operations/math.ts +128 -0
  45. package/src/registry/operations/membership.ts +132 -0
  46. package/src/registry/operations/string.ts +507 -0
  47. package/src/registry/operations/subsetting.ts +174 -0
  48. package/src/registry/operations/type-checking.ts +162 -0
  49. package/src/registry/operations/type-conversion.ts +404 -0
  50. package/src/registry/operations/type-operators.ts +307 -0
  51. package/src/registry/operations/utility.ts +542 -0
  52. package/src/registry/registry.ts +146 -0
  53. package/src/registry/types.ts +161 -0
  54. package/src/registry/utils/evaluation-helpers.ts +93 -0
  55. package/src/registry/utils/index.ts +3 -0
  56. package/src/registry/utils/type-system.ts +173 -0
  57. package/src/runtime/context.ts +179 -0
@@ -0,0 +1,161 @@
1
+ import type { TokenType } from '../lexer/token';
2
+ import type { TypeRef, ModelProvider } from '../analyzer/types';
3
+ import type { EvaluationResult } from '../interpreter/types';
4
+ import type { RuntimeContext } from '../runtime/context';
5
+
6
+ // Type information returned by analyze
7
+ export interface TypeInfo {
8
+ type: TypeRef;
9
+ isSingleton: boolean;
10
+ }
11
+
12
+ // Base interface for all operations
13
+ export interface BaseOperation {
14
+ name: string;
15
+
16
+ // Common lifecycle methods
17
+ analyze: (analyzer: Analyzer, input: TypeInfo, args: TypeInfo[]) => TypeInfo;
18
+ evaluate: (interpreter: Interpreter, context: RuntimeContext, input: any[], ...args: any[]) => EvaluationResult;
19
+ compile: (compiler: Compiler, input: CompiledExpression, args: CompiledExpression[]) => CompiledExpression;
20
+ }
21
+
22
+ // Operator-specific interface
23
+ export interface Operator extends BaseOperation {
24
+ kind: 'operator';
25
+
26
+ syntax: {
27
+ form: 'prefix' | 'infix' | 'postfix';
28
+ token: TokenType;
29
+ precedence: number;
30
+ associativity?: 'left' | 'right'; // For infix operators
31
+ notation: string; // e.g., "a + b", "not a"
32
+ special?: boolean; // For special forms like . and []
33
+ endToken?: TokenType; // For bracketed operators like []
34
+ };
35
+
36
+ signature: {
37
+ parameters: [OperatorParameter] | [OperatorParameter, OperatorParameter]; // Unary or binary
38
+ output: {
39
+ type: TypeInferenceRule;
40
+ cardinality: CardinalityInferenceRule;
41
+ };
42
+ propagatesEmpty?: boolean;
43
+ };
44
+ }
45
+
46
+ // Function-specific interface
47
+ export interface Function extends BaseOperation {
48
+ kind: 'function';
49
+
50
+ syntax: {
51
+ notation: string; // e.g., "substring(start [, length])"
52
+ };
53
+
54
+ signature: {
55
+ input?: {
56
+ types?: TypeConstraint;
57
+ cardinality?: 'singleton' | 'collection' | 'any';
58
+ };
59
+ parameters: FunctionParameter[];
60
+ output: {
61
+ type: TypeInferenceRule;
62
+ cardinality: CardinalityInferenceRule;
63
+ };
64
+ propagatesEmpty?: boolean;
65
+ deterministic?: boolean;
66
+ };
67
+ }
68
+
69
+ // Literal-specific interface
70
+ export interface Literal extends BaseOperation {
71
+ kind: 'literal';
72
+
73
+ syntax: {
74
+ pattern?: RegExp; // For complex literals like dates
75
+ keywords?: string[]; // For keyword literals like 'true', 'false'
76
+ notation: string; // Example: "123", "@2023-01-01"
77
+ };
78
+
79
+ // Literals have fixed output type
80
+ signature: {
81
+ output: {
82
+ type: string; // Always a specific type
83
+ cardinality: 'singleton'; // Literals are always singleton
84
+ };
85
+ };
86
+
87
+ // Parse the literal value from source text
88
+ parse: (value: string) => any;
89
+ }
90
+
91
+ // Union type for registry
92
+ export type Operation = Operator | Function | Literal;
93
+
94
+ // Specialized parameter types
95
+ export interface OperatorParameter {
96
+ name: 'left' | 'right' | 'operand';
97
+ types?: TypeConstraint;
98
+ cardinality?: 'singleton' | 'collection' | 'any'; // Required cardinality for this parameter
99
+ }
100
+
101
+ export interface FunctionParameter {
102
+ name: string;
103
+ kind: 'value' | 'expression' | 'type-specifier';
104
+ types?: TypeConstraint;
105
+ cardinality?: 'singleton' | 'collection' | 'any';
106
+ optional?: boolean;
107
+ default?: any;
108
+ }
109
+
110
+ export interface TypeConstraint {
111
+ kind: 'primitive' | 'class' | 'union' | 'any';
112
+ types?: string[]; // ['Integer', 'Decimal'] for numeric, ['Resource'] for FHIR types
113
+ }
114
+
115
+ export type TypeInferenceRule =
116
+ | string // Fixed type like 'Boolean'
117
+ | 'preserve-input' // Returns input type
118
+ | 'promote-numeric' // Integer + Decimal = Decimal
119
+ | ((input: TypeRef, args: TypeRef[], provider: ModelProvider) => TypeRef);
120
+
121
+ export type CardinalityInferenceRule =
122
+ | 'singleton' | 'collection'
123
+ | 'preserve-input' // Same as input
124
+ | 'all-singleton' // Singleton only if all inputs are singleton
125
+ | ((input: boolean, args: boolean[]) => boolean);
126
+
127
+ // Closure-based compiled expression
128
+ export interface CompiledExpression {
129
+ // The compiled function
130
+ fn: (context: RuntimeContext) => any[];
131
+
132
+ // Type information for optimization
133
+ type: TypeRef;
134
+ isSingleton: boolean;
135
+
136
+ // For debugging/tracing
137
+ source?: string;
138
+ }
139
+
140
+
141
+ // Interfaces for components that use the registry
142
+ export interface Analyzer {
143
+ error(message: string): void;
144
+ warning(message: string): void;
145
+ resolveType(typeName: string): TypeRef;
146
+ }
147
+
148
+ export interface Interpreter {
149
+ evaluate(node: any, input: any[], context: RuntimeContext): EvaluationResult;
150
+ }
151
+
152
+ export interface Compiler {
153
+ compile(node: any, input: CompiledExpression): CompiledExpression;
154
+ resolveType(typeName: string): TypeRef;
155
+ }
156
+
157
+ // Re-export TypeRef from analyzer
158
+ export type { TypeRef } from '../analyzer/types';
159
+
160
+ // Re-export RuntimeContext for use in other modules
161
+ export type { RuntimeContext } from '../runtime/context';
@@ -0,0 +1,93 @@
1
+ import { CollectionUtils } from '../../interpreter/types';
2
+
3
+ /**
4
+ * Convert collection to singleton value
5
+ */
6
+ export function toSingleton(collection: any[]): any {
7
+ return CollectionUtils.toSingleton(collection);
8
+ }
9
+
10
+ /**
11
+ * Convert value to boolean according to FHIRPath rules
12
+ */
13
+ export function toBoolean(value: any): boolean {
14
+ // Handle direct boolean
15
+ if (typeof value === 'boolean') {
16
+ return value;
17
+ }
18
+
19
+ // Handle null/undefined as false
20
+ if (value == null) {
21
+ return false;
22
+ }
23
+
24
+ // Handle empty string as false
25
+ if (value === '') {
26
+ return false;
27
+ }
28
+
29
+ // Handle zero as false
30
+ if (value === 0) {
31
+ return false;
32
+ }
33
+
34
+ // Everything else is true
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * Determines if a value is truthy according to FHIRPath rules
40
+ */
41
+ export function isTruthy(value: any[]): boolean {
42
+ if (value.length === 0) {
43
+ return false;
44
+ }
45
+
46
+ // Convert to singleton
47
+ const singleton = toSingleton(value);
48
+
49
+ // Rule: true if singleton is true, false if singleton is false
50
+ if (typeof singleton === 'boolean') {
51
+ return singleton;
52
+ }
53
+
54
+ // Rule: singleton exists and is not false = true
55
+ return singleton !== undefined;
56
+ }
57
+
58
+ /**
59
+ * Checks if two values are equivalent for FHIRPath comparison
60
+ */
61
+ export function isEquivalent(a: any, b: any): boolean {
62
+ // Handle null/undefined
63
+ if (a === b) return true;
64
+ if (a == null || b == null) return false;
65
+
66
+ // Handle different types
67
+ if (typeof a !== typeof b) return false;
68
+
69
+ // Handle primitives
70
+ if (typeof a !== 'object') return a === b;
71
+
72
+ // Handle arrays
73
+ if (Array.isArray(a) && Array.isArray(b)) {
74
+ if (a.length !== b.length) return false;
75
+ for (let i = 0; i < a.length; i++) {
76
+ if (!isEquivalent(a[i], b[i])) return false;
77
+ }
78
+ return true;
79
+ }
80
+
81
+ // Handle objects (deep comparison)
82
+ const keysA = Object.keys(a);
83
+ const keysB = Object.keys(b);
84
+
85
+ if (keysA.length !== keysB.length) return false;
86
+
87
+ for (const key of keysA) {
88
+ if (!keysB.includes(key)) return false;
89
+ if (!isEquivalent(a[key], b[key])) return false;
90
+ }
91
+
92
+ return true;
93
+ }
@@ -0,0 +1,3 @@
1
+ export { TypeSystem } from './type-system';
2
+ export { isTruthy, isEquivalent, toSingleton, toBoolean } from './evaluation-helpers';
3
+ export type { TypeInfo, PrimitiveType } from './type-system';
@@ -0,0 +1,173 @@
1
+ /**
2
+ * FHIRPath Type System
3
+ *
4
+ * Handles type checking and casting for FHIRPath expressions.
5
+ * Supports primitive types and FHIR resource types.
6
+ */
7
+
8
+ export enum PrimitiveType {
9
+ Boolean = 'Boolean',
10
+ String = 'String',
11
+ Integer = 'Integer',
12
+ Decimal = 'Decimal',
13
+ Date = 'Date',
14
+ DateTime = 'DateTime',
15
+ Time = 'Time',
16
+ Quantity = 'Quantity'
17
+ }
18
+
19
+ /**
20
+ * Type information for a value
21
+ */
22
+ export interface TypeInfo {
23
+ name: string;
24
+ isPrimitive: boolean;
25
+ isResource?: boolean;
26
+ isList?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Type registry for checking and casting
31
+ */
32
+ export class TypeSystem {
33
+ private static primitiveTypes = new Set<string>(Object.values(PrimitiveType));
34
+
35
+ /**
36
+ * Check if a value is of a specific type
37
+ */
38
+ static isType(value: any, typeName: string): boolean {
39
+ // Handle primitive types
40
+ if (this.primitiveTypes.has(typeName)) {
41
+ return this.isPrimitiveType(value, typeName as PrimitiveType);
42
+ }
43
+
44
+ // Handle FHIR resource types
45
+ // For now, check if the object has a resourceType property matching the type name
46
+ if (typeof value === 'object' && value !== null) {
47
+ return value.resourceType === typeName;
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Check if a value is a primitive type
55
+ */
56
+ private static isPrimitiveType(value: any, type: PrimitiveType): boolean {
57
+ switch (type) {
58
+ case PrimitiveType.Boolean:
59
+ return typeof value === 'boolean';
60
+
61
+ case PrimitiveType.String:
62
+ return typeof value === 'string';
63
+
64
+ case PrimitiveType.Integer:
65
+ return typeof value === 'number' && Number.isInteger(value);
66
+
67
+ case PrimitiveType.Decimal:
68
+ return typeof value === 'number';
69
+
70
+ case PrimitiveType.Date:
71
+ // Check if it's a date string in FHIR format (YYYY-MM-DD)
72
+ if (typeof value === 'string') {
73
+ return /^\d{4}(-\d{2}(-\d{2})?)?$/.test(value);
74
+ }
75
+ return false;
76
+
77
+ case PrimitiveType.DateTime:
78
+ // Check if it's a datetime string in FHIR format
79
+ if (typeof value === 'string') {
80
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?$/.test(value);
81
+ }
82
+ return false;
83
+
84
+ case PrimitiveType.Time:
85
+ // Check if it's a time string in FHIR format (HH:MM:SS)
86
+ if (typeof value === 'string') {
87
+ return /^\d{2}:\d{2}:\d{2}(\.\d{3})?$/.test(value);
88
+ }
89
+ return false;
90
+
91
+ case PrimitiveType.Quantity:
92
+ // Check if it's a quantity object with value and optional unit
93
+ if (typeof value === 'object' && value !== null) {
94
+ return typeof value.value === 'number' &&
95
+ (value.unit === undefined || typeof value.unit === 'string');
96
+ }
97
+ return false;
98
+
99
+ default:
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Cast a value to a specific type (returns empty if cast fails)
106
+ */
107
+ static cast(value: any, typeName: string): any | null {
108
+ // If already the correct type, return as-is
109
+ if (this.isType(value, typeName)) {
110
+ return value;
111
+ }
112
+
113
+ // Handle FHIR resource casting
114
+ if (!this.primitiveTypes.has(typeName)) {
115
+ // Can't cast between different resource types
116
+ return null;
117
+ }
118
+
119
+ // Handle primitive type casting
120
+ return this.castToPrimitive(value, typeName as PrimitiveType);
121
+ }
122
+
123
+ /**
124
+ * Cast to primitive type
125
+ */
126
+ private static castToPrimitive(value: any, type: PrimitiveType): any | null {
127
+ // For now, return null for failed casts
128
+ // Later we can implement actual conversions (e.g., string to number)
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Get type information for a value
134
+ */
135
+ static getType(value: any): TypeInfo {
136
+ // Check primitive types
137
+ if (typeof value === 'boolean') {
138
+ return { name: PrimitiveType.Boolean, isPrimitive: true };
139
+ }
140
+ if (typeof value === 'string') {
141
+ // Check for specific string formats
142
+ if (/^\d{4}(-\d{2}(-\d{2})?)?$/.test(value)) {
143
+ return { name: PrimitiveType.Date, isPrimitive: true };
144
+ }
145
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
146
+ return { name: PrimitiveType.DateTime, isPrimitive: true };
147
+ }
148
+ if (/^\d{2}:\d{2}:\d{2}/.test(value)) {
149
+ return { name: PrimitiveType.Time, isPrimitive: true };
150
+ }
151
+ return { name: PrimitiveType.String, isPrimitive: true };
152
+ }
153
+ if (typeof value === 'number') {
154
+ return {
155
+ name: Number.isInteger(value) ? PrimitiveType.Integer : PrimitiveType.Decimal,
156
+ isPrimitive: true
157
+ };
158
+ }
159
+
160
+ // Check for FHIR resources
161
+ if (typeof value === 'object' && value !== null) {
162
+ if (value.resourceType) {
163
+ return { name: value.resourceType, isPrimitive: false, isResource: true };
164
+ }
165
+ if (typeof value.value === 'number' && (value.unit || value.code)) {
166
+ return { name: PrimitiveType.Quantity, isPrimitive: true };
167
+ }
168
+ }
169
+
170
+ // Unknown type
171
+ return { name: 'Unknown', isPrimitive: false };
172
+ }
173
+ }
@@ -0,0 +1,179 @@
1
+ import type { Context } from '../interpreter/types';
2
+
3
+ /**
4
+ * Unified runtime context that works with both interpreter and compiler.
5
+ * Uses prototype-based inheritance for efficient context copying.
6
+ */
7
+ export interface RuntimeContext {
8
+ input: any[];
9
+ focus: any[];
10
+ env: {
11
+ $this?: any[];
12
+ $index?: number;
13
+ $total?: any[];
14
+ $context?: any[];
15
+ $resource?: any[];
16
+ $rootResource?: any[];
17
+ [key: string]: any;
18
+ };
19
+ variables?: Record<string, any[]>;
20
+ }
21
+
22
+ /**
23
+ * Runtime context manager that provides efficient prototype-based context operations
24
+ * for both interpreter and compiler.
25
+ */
26
+ export class RuntimeContextManager {
27
+ /**
28
+ * Create a new runtime context
29
+ */
30
+ static create(input: any[], initialEnv?: Record<string, any>): RuntimeContext {
31
+ const context = Object.create(null) as RuntimeContext;
32
+
33
+ context.input = input;
34
+ context.focus = input;
35
+
36
+ // Create env with null prototype to avoid pollution
37
+ context.env = Object.create(null);
38
+ if (initialEnv) {
39
+ Object.assign(context.env, initialEnv);
40
+ }
41
+
42
+ // Set root context variables
43
+ context.env.$context = input;
44
+ context.env.$resource = input;
45
+ context.env.$rootResource = input;
46
+
47
+ // Create variables object
48
+ context.variables = Object.create(null);
49
+
50
+ return context;
51
+ }
52
+
53
+ /**
54
+ * Create a child context using prototype inheritance
55
+ * O(1) operation - no copying needed
56
+ */
57
+ static copy(context: RuntimeContext): RuntimeContext {
58
+ // Create child context with parent as prototype
59
+ const newContext = Object.create(context) as RuntimeContext;
60
+
61
+ // Create child env that inherits from parent's env
62
+ newContext.env = Object.create(context.env);
63
+
64
+ // Create child variables that inherit from parent's variables
65
+ if (context.variables) {
66
+ newContext.variables = Object.create(context.variables);
67
+ }
68
+
69
+ // input and focus are inherited through prototype chain
70
+ // Only set them if they need to change
71
+
72
+ return newContext;
73
+ }
74
+
75
+ /**
76
+ * Create a new context with updated input/focus
77
+ */
78
+ static withInput(context: RuntimeContext, input: any[], focus?: any[]): RuntimeContext {
79
+ const newContext = this.copy(context);
80
+ newContext.input = input;
81
+ newContext.focus = focus ?? input;
82
+ return newContext;
83
+ }
84
+
85
+ /**
86
+ * Set iterator context ($this, $index)
87
+ */
88
+ static withIterator(
89
+ context: RuntimeContext,
90
+ item: any,
91
+ index: number
92
+ ): RuntimeContext {
93
+ const newContext = this.copy(context);
94
+ newContext.env.$this = [item];
95
+ newContext.env.$index = index;
96
+ newContext.input = [item];
97
+ newContext.focus = [item];
98
+ return newContext;
99
+ }
100
+
101
+ /**
102
+ * Set a variable in the context
103
+ */
104
+ static setVariable(context: RuntimeContext, name: string, value: any[]): RuntimeContext {
105
+ const newContext = this.copy(context);
106
+ if (!newContext.variables) {
107
+ newContext.variables = Object.create(null);
108
+ }
109
+ newContext.variables![name] = value;
110
+ return newContext;
111
+ }
112
+
113
+ /**
114
+ * Get a variable from context (handles special variables too)
115
+ */
116
+ static getVariable(context: RuntimeContext, name: string): any[] | undefined {
117
+ // Remove % prefix if present
118
+ const varName = name.startsWith('%') ? name.substring(1) : name;
119
+
120
+ // Check special variables first
121
+ switch (varName) {
122
+ case 'context':
123
+ return context.env.$context || context.input;
124
+ case 'resource':
125
+ return context.env.$resource || context.input;
126
+ case 'rootResource':
127
+ return context.env.$rootResource || context.input;
128
+ default:
129
+ // Check user-defined variables
130
+ return context.variables?.[varName];
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Convert from interpreter Context to RuntimeContext
136
+ */
137
+ static fromContext(context: Context, input: any[]): RuntimeContext {
138
+ const rtContext = this.create(input);
139
+
140
+ // Copy variables
141
+ if (context.variables) {
142
+ rtContext.variables = Object.create(context.variables);
143
+ }
144
+
145
+ // Copy environment
146
+ Object.assign(rtContext.env, context.env);
147
+
148
+ // Copy root variables
149
+ if ((context as any).$context) rtContext.env.$context = (context as any).$context;
150
+ if ((context as any).$resource) rtContext.env.$resource = (context as any).$resource;
151
+ if ((context as any).$rootResource) rtContext.env.$rootResource = (context as any).$rootResource;
152
+
153
+ return rtContext;
154
+ }
155
+
156
+ /**
157
+ * Convert from RuntimeContext to interpreter Context
158
+ */
159
+ static toContext(rtContext: RuntimeContext): Context {
160
+ const context = Object.create(null) as Context;
161
+
162
+ // Copy variables
163
+ context.variables = rtContext.variables || Object.create(null);
164
+
165
+ // Extract env variables
166
+ context.env = {
167
+ $this: rtContext.env.$this,
168
+ $index: rtContext.env.$index,
169
+ $total: rtContext.env.$total
170
+ };
171
+
172
+ // Extract root variables
173
+ (context as any).$context = rtContext.env.$context;
174
+ (context as any).$resource = rtContext.env.$resource;
175
+ (context as any).$rootResource = rtContext.env.$rootResource;
176
+
177
+ return context;
178
+ }
179
+ }