@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.
- package/README.md +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8185 -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 +152 -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 +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -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 +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -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 +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -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
- 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,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
|
+
}
|