@atomic-ehr/fhirpath 0.0.1-canary.0c6931e.20250727185306
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 +473 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +10307 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/analyzer/analyzer.ts +499 -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 +157 -0
- package/src/api/errors.ts +145 -0
- package/src/api/expression.ts +156 -0
- package/src/api/index.ts +122 -0
- package/src/api/inspect.ts +99 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +210 -0
- package/src/compiler/compiler.ts +546 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +24 -0
- package/src/index.ts +107 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +475 -0
- package/src/interpreter/types.ts +108 -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/lexer2/index.md +232 -0
- package/src/lexer2/index.perf.test.ts +68 -0
- package/src/lexer2/index.test.ts +549 -0
- package/src/lexer2/index.ts +1251 -0
- package/src/lexer2/notes.md +173 -0
- package/src/lexer2/optimization-summary.md +718 -0
- package/src/parser/ast-factory.ts +220 -0
- package/src/parser/ast.ts +144 -0
- package/src/parser/collection-parser.ts +89 -0
- package/src/parser/diagnostic-messages.ts +216 -0
- package/src/parser/diagnostics.ts +85 -0
- package/src/parser/error-reporter.ts +230 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/literal-parser.ts +103 -0
- package/src/parser/parse-error.ts +16 -0
- package/src/parser/parser-error-factory.ts +141 -0
- package/src/parser/parser-state.ts +134 -0
- package/src/parser/parser.ts +1272 -0
- package/src/parser/pprint.ts +169 -0
- package/src/parser/precedence-manager.ts +64 -0
- package/src/parser/source-mapper.ts +248 -0
- package/src/parser/special-constructs.ts +142 -0
- package/src/parser/token-navigator.ts +110 -0
- package/src/parser/types.ts +60 -0
- package/src/parser2/index.md +177 -0
- package/src/parser2/index.perf.test.ts +184 -0
- package/src/parser2/index.test.ts +305 -0
- package/src/parser2/index.ts +578 -0
- package/src/parser2/optimization-summary.md +176 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +96 -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 +439 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/navigation.ts +52 -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 +308 -0
- package/src/registry/operations/utility.ts +644 -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 +158 -0
- package/src/runtime/debug-context.ts +135 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Parser2 Performance Optimization Summary
|
|
2
|
+
|
|
3
|
+
## Baseline Performance (2024-01-27)
|
|
4
|
+
|
|
5
|
+
### Test Configuration
|
|
6
|
+
- **Test Suite**: 1,539 FHIRPath expressions from real-world invariants and search parameters
|
|
7
|
+
- **Iterations**: 5,000 per expression (7,695,000 total parser runs)
|
|
8
|
+
- **Hardware**: Bun v1.2.18 runtime
|
|
9
|
+
- **Parser**: Recursive-descent with Pratt operator precedence parsing
|
|
10
|
+
|
|
11
|
+
### Baseline Metrics
|
|
12
|
+
|
|
13
|
+
| Metric | Value |
|
|
14
|
+
|--------|-------|
|
|
15
|
+
| **Total time** | 6.51 seconds |
|
|
16
|
+
| **Time per expression** | 0.0008ms (0.8 microseconds) |
|
|
17
|
+
| **Expressions per second** | 1,182,443 |
|
|
18
|
+
| **Average tokens per expression** | 11.2 |
|
|
19
|
+
| **Average AST nodes per expression** | 8.6 |
|
|
20
|
+
| **Time per token** | ~0.08 microseconds |
|
|
21
|
+
|
|
22
|
+
### Performance by Expression Complexity
|
|
23
|
+
|
|
24
|
+
| Token Range | Count | Avg Time (ms) | Time/Token (μs) |
|
|
25
|
+
|-------------|-------|---------------|-----------------|
|
|
26
|
+
| 1-5 tokens | 716 | 0.0003 | 0.08 |
|
|
27
|
+
| 6-10 tokens | 410 | 0.0005 | 0.08 |
|
|
28
|
+
| 11-20 tokens | 251 | 0.0010 | 0.07 |
|
|
29
|
+
| 21-50 tokens | 130 | 0.0024 | 0.08 |
|
|
30
|
+
| 50+ tokens | 32 | 0.0093 | 0.08 |
|
|
31
|
+
|
|
32
|
+
### Key Observations
|
|
33
|
+
|
|
34
|
+
1. **Linear Scaling**: Performance scales linearly with token count, indicating O(n) complexity
|
|
35
|
+
2. **Consistent Token Processing**: ~0.08μs per token across all complexity ranges
|
|
36
|
+
3. **Fast Baseline**: Already processing over 1 million expressions per second
|
|
37
|
+
4. **Memory Efficiency**: All tokens are pre-loaded, enabling random access without re-parsing
|
|
38
|
+
|
|
39
|
+
### Slowest Expressions
|
|
40
|
+
|
|
41
|
+
The most complex expression has 780 tokens and takes 0.0589ms to parse:
|
|
42
|
+
```
|
|
43
|
+
(ActivityDefinition.useContext.value...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This represents a worst-case scenario that's still processed in under 0.06ms.
|
|
47
|
+
|
|
48
|
+
## Design Strengths
|
|
49
|
+
|
|
50
|
+
1. **Pratt Parsing**: Eliminates recursive precedence climbing, reducing call stack depth
|
|
51
|
+
2. **Pre-tokenization**: All tokens generated upfront, avoiding interleaved lexing/parsing
|
|
52
|
+
3. **Direct Token Access**: Array-based token storage enables O(1) lookahead
|
|
53
|
+
4. **Minimal Allocations**: AST nodes created only when needed
|
|
54
|
+
5. **Type-safe Node Creation**: Separate factory methods avoid dynamic object construction
|
|
55
|
+
|
|
56
|
+
## Comparison with Lexer2
|
|
57
|
+
|
|
58
|
+
| Component | Expressions/sec | Relative Speed |
|
|
59
|
+
|-----------|----------------|----------------|
|
|
60
|
+
| Lexer2 | ~15,000,000 | 1.0x (baseline) |
|
|
61
|
+
| Parser2 | ~1,200,000 | 0.08x |
|
|
62
|
+
|
|
63
|
+
The parser is ~12x slower than the lexer, which is expected since it:
|
|
64
|
+
- Builds a complete AST structure
|
|
65
|
+
- Performs syntax validation
|
|
66
|
+
- Handles operator precedence
|
|
67
|
+
- Manages recursive structures
|
|
68
|
+
|
|
69
|
+
## Potential Optimizations
|
|
70
|
+
|
|
71
|
+
While the baseline performance is already excellent, potential optimizations include:
|
|
72
|
+
|
|
73
|
+
1. **Token Caching**: Cache frequently accessed token values to avoid repeated string operations
|
|
74
|
+
2. **Specialized Node Pools**: Pre-allocate common node types to reduce allocation overhead
|
|
75
|
+
3. **Inline Small Functions**: Further inline hot-path functions like `peek()` and `check()`
|
|
76
|
+
4. **Precedence Table**: Replace switch statement with lookup table for operator precedence
|
|
77
|
+
5. **String Interning**: Intern common identifier strings to reduce memory and comparison costs
|
|
78
|
+
|
|
79
|
+
## ANTLR Parser Comparison
|
|
80
|
+
|
|
81
|
+
To provide additional context, we compared parser2 performance against the ANTLR-generated parser:
|
|
82
|
+
|
|
83
|
+
### Overall Performance Comparison
|
|
84
|
+
|
|
85
|
+
| Parser | Expressions/sec | Time per Expression | Relative Performance |
|
|
86
|
+
|--------|----------------|-------------------|---------------------|
|
|
87
|
+
| ANTLR | 143,965 | 0.0069ms | 1.0x (baseline) |
|
|
88
|
+
| Parser2 | 1,030,046 | 0.0010ms | **7.2x faster** |
|
|
89
|
+
|
|
90
|
+
### Complex Expression Performance
|
|
91
|
+
|
|
92
|
+
| Expression | ANTLR (ms) | Parser2 (ms) | Speedup |
|
|
93
|
+
|------------|------------|--------------|---------|
|
|
94
|
+
| Patient.name.where(use = 'official').given | 0.009 | 0.001 | 7.7x |
|
|
95
|
+
| (ActivityDefinition.useContext.value... | 0.009 | 0.001 | 7.1x |
|
|
96
|
+
| ($this is Range) implies ((low.empty()... | 0.027 | 0.002 | 11.2x |
|
|
97
|
+
| extension.exists() or (contentType.co... | 0.047 | 0.001 | 33.1x |
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
### Key Differences
|
|
102
|
+
|
|
103
|
+
1. **Architecture**: ANTLR uses table-driven parsing while parser2 uses recursive descent with Pratt parsing
|
|
104
|
+
2. **Overhead**: ANTLR has more runtime overhead due to its generic parsing framework
|
|
105
|
+
3. **Scalability**: Parser2 shows better performance on complex expressions (up to 33x faster)
|
|
106
|
+
4. **Error Recovery**: ANTLR provides better error recovery but at a performance cost
|
|
107
|
+
|
|
108
|
+
## Optimization: Bit-Packed Precedence (2024-01-27)
|
|
109
|
+
|
|
110
|
+
### Implementation
|
|
111
|
+
Encoded operator precedence directly into TokenType enum values using bit-packing:
|
|
112
|
+
- High byte: Precedence (0-255)
|
|
113
|
+
- Low byte: Token ID (0-255)
|
|
114
|
+
|
|
115
|
+
### Results
|
|
116
|
+
| Metric | Before | After | Improvement |
|
|
117
|
+
|--------|--------|-------|-------------|
|
|
118
|
+
| Expressions/sec | 1,182,443 | 1,214,209 | +2.7% |
|
|
119
|
+
| Time per expression | 0.8μs | 0.8μs | - |
|
|
120
|
+
| getPrecedence cycles | ~5-10 | ~1 | -90% |
|
|
121
|
+
|
|
122
|
+
### Code Simplification
|
|
123
|
+
- Removed 30+ line switch statement
|
|
124
|
+
- Replaced with single bit shift: `type >>> 8`
|
|
125
|
+
- Precedence now co-located with token definitions
|
|
126
|
+
|
|
127
|
+
### Note
|
|
128
|
+
A direct precedence value approach (without bit-packing) was also tested but resulted in 8.8% performance degradation due to lookup table overhead. The bit-packed approach with bit shift operation provides the best balance of performance and maintainability.
|
|
129
|
+
|
|
130
|
+
## Additional Optimizations (2024-01-27)
|
|
131
|
+
|
|
132
|
+
### 1. Method Inlining
|
|
133
|
+
Inlined hot path methods (`peek()`, `advance()`, `isAtEnd()`) to reduce function call overhead in `parseExpressionWithPrecedence()` and `parsePrimary()`.
|
|
134
|
+
|
|
135
|
+
**Results**:
|
|
136
|
+
- Baseline: 1,203,640 expressions/sec
|
|
137
|
+
- After inlining: 1,223,027 expressions/sec (+1.6%)
|
|
138
|
+
|
|
139
|
+
### 2. Node Creation Inlining (Reverted)
|
|
140
|
+
Attempted to inline frequently used node creation functions (`createBinaryNode`, `createLiteralNode`, `createIdentifierNode`) but observed performance regression due to increased code size affecting CPU cache and JIT optimization.
|
|
141
|
+
|
|
142
|
+
**Results**: Performance decreased to 1,184,463 expressions/sec - optimization was reverted.
|
|
143
|
+
|
|
144
|
+
### 3. Lookup Tables for Token Type Checking (Abandoned)
|
|
145
|
+
Attempted to use Uint8Array lookup tables for binary operator and keyword checking, but abandoned due to bit-packed token values exceeding array bounds (values > 256).
|
|
146
|
+
|
|
147
|
+
### 4. Position Tracking Impact Analysis
|
|
148
|
+
Temporarily disabled position tracking to measure its performance cost.
|
|
149
|
+
|
|
150
|
+
**Results**:
|
|
151
|
+
- With position tracking: 1,226,319 expressions/sec
|
|
152
|
+
- Without position tracking: 1,212,703 expressions/sec (-1.1%)
|
|
153
|
+
|
|
154
|
+
**Surprising finding**: Removing position tracking actually decreased performance slightly, likely due to:
|
|
155
|
+
- Position tracking overhead is minimal (simple object property assignment)
|
|
156
|
+
- Code changes disrupted JIT compiler optimizations
|
|
157
|
+
- Modern JS engines optimize object creation very well
|
|
158
|
+
|
|
159
|
+
### Key Findings:
|
|
160
|
+
- Simple method inlining in hot paths provides consistent gains
|
|
161
|
+
- Over-optimization (excessive inlining) can hurt performance
|
|
162
|
+
- JIT compiler already optimizes method calls effectively
|
|
163
|
+
- Position tracking has negligible performance impact
|
|
164
|
+
- Bit-packed TokenType with getPrecedence bit shift remains the best optimization
|
|
165
|
+
- Current optimized performance: **1,223,027 expressions/sec** (1.6% improvement)
|
|
166
|
+
|
|
167
|
+
## Conclusion
|
|
168
|
+
|
|
169
|
+
The parser2 performance is exceptional:
|
|
170
|
+
- **7.2x faster** than the ANTLR-generated parser
|
|
171
|
+
- Processing over **1.2 million expressions per second**
|
|
172
|
+
- Consistent **linear O(n) scaling** with token count
|
|
173
|
+
- Sub-microsecond parsing times for typical expressions
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
The combination of recursive-descent parsing with Pratt operator precedence, enhanced with bit-packed precedence lookup, provides an optimal balance of simplicity, maintainability, and performance. Even with already excellent baseline performance, targeted optimizations like bit-packed precedence can provide measurable improvements while simplifying the codebase.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Analyzer,
|
|
3
|
+
TypeInfo,
|
|
4
|
+
TypeRef,
|
|
5
|
+
Operator,
|
|
6
|
+
Function,
|
|
7
|
+
Literal,
|
|
8
|
+
TypeConstraint,
|
|
9
|
+
TypeInferenceRule,
|
|
10
|
+
CardinalityInferenceRule
|
|
11
|
+
} from './types';
|
|
12
|
+
import type { ModelProvider } from '../analyzer/types';
|
|
13
|
+
|
|
14
|
+
// Helper to check if type matches constraint
|
|
15
|
+
export function matchesConstraint(type: TypeRef, constraint: TypeConstraint): boolean {
|
|
16
|
+
if (constraint.kind === 'any') return true;
|
|
17
|
+
|
|
18
|
+
if (!constraint.types) return true;
|
|
19
|
+
|
|
20
|
+
// For now, simple type name matching
|
|
21
|
+
// TODO: Handle inheritance and complex types
|
|
22
|
+
let typeName = typeof type === 'string' ? type : (type as any).type || (type as any).name || 'unknown';
|
|
23
|
+
|
|
24
|
+
// Normalize case - constraint types are capitalized, but actual types might be lowercase
|
|
25
|
+
const normalizedTypeName = typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
|
26
|
+
|
|
27
|
+
return constraint.types.includes(normalizedTypeName) || constraint.types.includes(typeName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper to format constraint for error messages
|
|
31
|
+
export function formatConstraint(constraint: TypeConstraint): string {
|
|
32
|
+
if (constraint.kind === 'any') return 'any type';
|
|
33
|
+
if (!constraint.types || constraint.types.length === 0) return 'any type';
|
|
34
|
+
if (constraint.types.length === 1) return constraint.types[0]!;
|
|
35
|
+
return constraint.types.join(' or ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper to resolve type inference rule
|
|
39
|
+
export function resolveTypeInferenceRule(
|
|
40
|
+
rule: TypeInferenceRule,
|
|
41
|
+
input: TypeRef,
|
|
42
|
+
args: TypeRef[],
|
|
43
|
+
analyzer: Analyzer
|
|
44
|
+
): TypeRef {
|
|
45
|
+
if (typeof rule === 'string') {
|
|
46
|
+
if (rule === 'preserve-input') return input;
|
|
47
|
+
if (rule === 'promote-numeric') {
|
|
48
|
+
// Check if we have numeric types
|
|
49
|
+
const types = [input, ...args].map(t => {
|
|
50
|
+
const rawType = typeof t === 'string' ? t : (t as any).type || (t as any).name || 'unknown';
|
|
51
|
+
// Normalize to uppercase first letter
|
|
52
|
+
return rawType.charAt(0).toUpperCase() + rawType.slice(1);
|
|
53
|
+
});
|
|
54
|
+
if (types.includes('Decimal')) return analyzer.resolveType('Decimal');
|
|
55
|
+
if (types.includes('Integer')) return analyzer.resolveType('Integer');
|
|
56
|
+
return input; // fallback
|
|
57
|
+
}
|
|
58
|
+
return analyzer.resolveType(rule); // Fixed type name - resolve through analyzer
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof rule === 'function') {
|
|
62
|
+
// TODO: We need to pass ModelProvider to the rule function
|
|
63
|
+
// For now, just return input
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return input; // fallback
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Helper to resolve cardinality inference rule
|
|
71
|
+
export function resolveCardinalityInferenceRule(
|
|
72
|
+
rule: CardinalityInferenceRule,
|
|
73
|
+
inputIsSingleton: boolean,
|
|
74
|
+
argsAreSingleton: boolean[]
|
|
75
|
+
): boolean {
|
|
76
|
+
if (rule === 'singleton') return true;
|
|
77
|
+
if (rule === 'collection') return false;
|
|
78
|
+
if (rule === 'preserve-input') return inputIsSingleton;
|
|
79
|
+
if (rule === 'all-singleton') return inputIsSingleton && argsAreSingleton.every(s => s);
|
|
80
|
+
|
|
81
|
+
if (typeof rule === 'function') {
|
|
82
|
+
return rule(inputIsSingleton, argsAreSingleton);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return inputIsSingleton; // fallback
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Default analyzer for operators
|
|
89
|
+
export function defaultOperatorAnalyze(
|
|
90
|
+
this: Operator,
|
|
91
|
+
analyzer: Analyzer,
|
|
92
|
+
input: TypeInfo,
|
|
93
|
+
args: TypeInfo[]
|
|
94
|
+
): TypeInfo {
|
|
95
|
+
const { signature } = this;
|
|
96
|
+
|
|
97
|
+
// Validate parameter count
|
|
98
|
+
const expectedParams = signature.parameters.length;
|
|
99
|
+
if (args.length !== expectedParams) {
|
|
100
|
+
analyzer.error(`Operator '${this.name}' expects ${expectedParams} parameter(s) but got ${args.length}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate parameter types and cardinality
|
|
104
|
+
signature.parameters.forEach((param, i) => {
|
|
105
|
+
const arg = args[i];
|
|
106
|
+
if (!arg) return;
|
|
107
|
+
|
|
108
|
+
// Check type constraint
|
|
109
|
+
if (param.types && !matchesConstraint(arg.type, param.types)) {
|
|
110
|
+
const argTypeName = typeof arg.type === 'string'
|
|
111
|
+
? arg.type
|
|
112
|
+
: (arg.type as any).type || (arg.type as any).name || 'unknown';
|
|
113
|
+
analyzer.error(
|
|
114
|
+
`Operator '${this.name}' parameter '${param.name}' expects ${formatConstraint(param.types)} but got ${argTypeName}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check cardinality constraint
|
|
119
|
+
if (param.cardinality) {
|
|
120
|
+
if (param.cardinality === 'singleton' && !arg.isSingleton) {
|
|
121
|
+
analyzer.error(
|
|
122
|
+
`Operator '${this.name}' parameter '${param.name}' requires singleton value but got collection`
|
|
123
|
+
);
|
|
124
|
+
} else if (param.cardinality === 'collection' && arg.isSingleton) {
|
|
125
|
+
analyzer.error(
|
|
126
|
+
`Operator '${this.name}' parameter '${param.name}' requires collection but got singleton`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
// 'any' accepts both
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Determine output type
|
|
134
|
+
const outputType = resolveTypeInferenceRule(
|
|
135
|
+
signature.output.type,
|
|
136
|
+
input.type,
|
|
137
|
+
args.map(a => a.type),
|
|
138
|
+
analyzer
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Determine output cardinality
|
|
142
|
+
const isSingleton = resolveCardinalityInferenceRule(
|
|
143
|
+
signature.output.cardinality,
|
|
144
|
+
input.isSingleton,
|
|
145
|
+
args.map(a => a.isSingleton)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return { type: outputType, isSingleton };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Default analyzer for functions
|
|
152
|
+
export function defaultFunctionAnalyze(
|
|
153
|
+
this: Function,
|
|
154
|
+
analyzer: Analyzer,
|
|
155
|
+
input: TypeInfo,
|
|
156
|
+
args: TypeInfo[]
|
|
157
|
+
): TypeInfo {
|
|
158
|
+
const { signature } = this;
|
|
159
|
+
|
|
160
|
+
// Check input constraints if specified
|
|
161
|
+
if (signature.input) {
|
|
162
|
+
if (signature.input.types && !matchesConstraint(input.type, signature.input.types)) {
|
|
163
|
+
const inputTypeName = typeof input.type === 'string'
|
|
164
|
+
? input.type
|
|
165
|
+
: (input.type as any).type || (input.type as any).name || 'unknown';
|
|
166
|
+
analyzer.error(
|
|
167
|
+
`Function '${this.name}' expects input of type ${formatConstraint(signature.input.types)} but got ${inputTypeName}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (signature.input.cardinality) {
|
|
172
|
+
if (signature.input.cardinality === 'singleton' && !input.isSingleton) {
|
|
173
|
+
analyzer.error(`Function '${this.name}' requires singleton input but got collection`);
|
|
174
|
+
} else if (signature.input.cardinality === 'collection' && input.isSingleton) {
|
|
175
|
+
analyzer.error(`Function '${this.name}' requires collection input but got singleton`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Validate parameters
|
|
181
|
+
const requiredParams = signature.parameters.filter(p => !p.optional);
|
|
182
|
+
if (args.length < requiredParams.length) {
|
|
183
|
+
analyzer.error(
|
|
184
|
+
`Function '${this.name}' requires at least ${requiredParams.length} parameter(s) but got ${args.length}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (args.length > signature.parameters.length) {
|
|
189
|
+
analyzer.error(
|
|
190
|
+
`Function '${this.name}' expects at most ${signature.parameters.length} parameter(s) but got ${args.length}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check each parameter
|
|
195
|
+
signature.parameters.forEach((param, i) => {
|
|
196
|
+
const arg = args[i];
|
|
197
|
+
if (!arg && !param.optional) {
|
|
198
|
+
analyzer.error(`Function '${this.name}' parameter '${param.name}' is required`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!arg) return; // Optional parameter not provided
|
|
202
|
+
|
|
203
|
+
// Check type constraint
|
|
204
|
+
if (param.types && !matchesConstraint(arg.type, param.types)) {
|
|
205
|
+
const argTypeName = typeof arg.type === 'string'
|
|
206
|
+
? arg.type
|
|
207
|
+
: (arg.type as any).type || (arg.type as any).name || 'unknown';
|
|
208
|
+
analyzer.error(
|
|
209
|
+
`Function '${this.name}' parameter '${param.name}' expects ${formatConstraint(param.types)} but got ${argTypeName}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check cardinality constraint
|
|
214
|
+
if (param.cardinality) {
|
|
215
|
+
if (param.cardinality === 'singleton' && !arg.isSingleton) {
|
|
216
|
+
analyzer.error(
|
|
217
|
+
`Function '${this.name}' parameter '${param.name}' requires singleton value but got collection`
|
|
218
|
+
);
|
|
219
|
+
} else if (param.cardinality === 'collection' && arg.isSingleton) {
|
|
220
|
+
analyzer.error(
|
|
221
|
+
`Function '${this.name}' parameter '${param.name}' requires collection but got singleton`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Determine output type
|
|
228
|
+
const outputType = resolveTypeInferenceRule(
|
|
229
|
+
signature.output.type,
|
|
230
|
+
input.type,
|
|
231
|
+
args.map(a => a.type),
|
|
232
|
+
analyzer
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Determine output cardinality
|
|
236
|
+
const isSingleton = resolveCardinalityInferenceRule(
|
|
237
|
+
signature.output.cardinality,
|
|
238
|
+
input.isSingleton,
|
|
239
|
+
args.map(a => a.isSingleton)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return { type: outputType, isSingleton };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Default analyzer for literals
|
|
246
|
+
export function defaultLiteralAnalyze(
|
|
247
|
+
this: Literal,
|
|
248
|
+
analyzer: Analyzer,
|
|
249
|
+
input: TypeInfo,
|
|
250
|
+
args: TypeInfo[]
|
|
251
|
+
): TypeInfo {
|
|
252
|
+
// Literals have fixed output type and cardinality
|
|
253
|
+
return {
|
|
254
|
+
type: analyzer.resolveType(this.signature.output.type),
|
|
255
|
+
isSingleton: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Compiler, CompiledExpression } from './types';
|
|
2
|
+
|
|
3
|
+
export function defaultFunctionCompile(
|
|
4
|
+
compiler: Compiler,
|
|
5
|
+
input: CompiledExpression,
|
|
6
|
+
args: CompiledExpression[]
|
|
7
|
+
): CompiledExpression {
|
|
8
|
+
// Default implementation returns a simple pass-through
|
|
9
|
+
return {
|
|
10
|
+
fn: (ctx) => {
|
|
11
|
+
throw new Error('Function compile not implemented');
|
|
12
|
+
},
|
|
13
|
+
type: compiler.resolveType('Any'),
|
|
14
|
+
isSingleton: false
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defaultOperatorCompile(
|
|
19
|
+
compiler: Compiler,
|
|
20
|
+
left: CompiledExpression,
|
|
21
|
+
right: CompiledExpression[]
|
|
22
|
+
): CompiledExpression {
|
|
23
|
+
// Default implementation returns a simple pass-through
|
|
24
|
+
return {
|
|
25
|
+
fn: (ctx) => {
|
|
26
|
+
throw new Error('Operator compile not implemented');
|
|
27
|
+
},
|
|
28
|
+
type: compiler.resolveType('Any'),
|
|
29
|
+
isSingleton: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Registry } from './registry';
|
|
2
|
+
import { arithmeticOperators } from './operations/arithmetic';
|
|
3
|
+
import { logicalOperators, logicalFunctions } from './operations/logical';
|
|
4
|
+
import { comparisonOperators } from './operations/comparison';
|
|
5
|
+
import { membershipOperators } from './operations/membership';
|
|
6
|
+
import { typeOperators } from './operations/type-operators';
|
|
7
|
+
import { literals } from './operations/literals';
|
|
8
|
+
import { existenceFunctions } from './operations/existence';
|
|
9
|
+
import { navigationOperators } from './operations/navigation';
|
|
10
|
+
import {
|
|
11
|
+
aggregateFunction,
|
|
12
|
+
childrenFunction,
|
|
13
|
+
descendantsFunction,
|
|
14
|
+
iifFunction,
|
|
15
|
+
defineVariableFunction,
|
|
16
|
+
traceFunction,
|
|
17
|
+
checkFunction
|
|
18
|
+
} from './operations/utility';
|
|
19
|
+
import { typeFunction, isFunction, asFunction } from './operations/type-checking';
|
|
20
|
+
import { absFunction, roundFunction, sqrtFunction } from './operations/math';
|
|
21
|
+
import { whereFunction, selectFunction, ofTypeFunction, repeatFunction } from './operations/filtering';
|
|
22
|
+
import {
|
|
23
|
+
containsFunction, lengthFunction, substringFunction, startsWithFunction,
|
|
24
|
+
endsWithFunction, upperFunction, lowerFunction, indexOfFunction,
|
|
25
|
+
replaceFunction, splitFunction, joinFunction, trimFunction, toCharsFunction
|
|
26
|
+
} from './operations/string';
|
|
27
|
+
import {
|
|
28
|
+
toStringFunction, toIntegerFunction, toDecimalFunction,
|
|
29
|
+
toBooleanFunction, toQuantityFunction
|
|
30
|
+
} from './operations/type-conversion';
|
|
31
|
+
import { tailFunction, skipFunction, takeFunction } from './operations/subsetting';
|
|
32
|
+
import { unionFunction, combineFunction, intersectFunction, excludeFunction, collectionOperators } from './operations/collection';
|
|
33
|
+
|
|
34
|
+
// Export types
|
|
35
|
+
export * from './types';
|
|
36
|
+
export { Registry } from './registry';
|
|
37
|
+
export * from './default-analyzers';
|
|
38
|
+
|
|
39
|
+
// Register all operations on module load
|
|
40
|
+
[
|
|
41
|
+
...arithmeticOperators,
|
|
42
|
+
...logicalOperators,
|
|
43
|
+
...logicalFunctions,
|
|
44
|
+
...comparisonOperators,
|
|
45
|
+
...membershipOperators,
|
|
46
|
+
...typeOperators,
|
|
47
|
+
...collectionOperators,
|
|
48
|
+
...navigationOperators,
|
|
49
|
+
...literals,
|
|
50
|
+
...existenceFunctions,
|
|
51
|
+
aggregateFunction,
|
|
52
|
+
childrenFunction,
|
|
53
|
+
descendantsFunction,
|
|
54
|
+
iifFunction,
|
|
55
|
+
defineVariableFunction,
|
|
56
|
+
traceFunction,
|
|
57
|
+
checkFunction,
|
|
58
|
+
typeFunction,
|
|
59
|
+
isFunction,
|
|
60
|
+
asFunction,
|
|
61
|
+
absFunction,
|
|
62
|
+
roundFunction,
|
|
63
|
+
sqrtFunction,
|
|
64
|
+
whereFunction,
|
|
65
|
+
selectFunction,
|
|
66
|
+
ofTypeFunction,
|
|
67
|
+
repeatFunction,
|
|
68
|
+
containsFunction,
|
|
69
|
+
lengthFunction,
|
|
70
|
+
substringFunction,
|
|
71
|
+
startsWithFunction,
|
|
72
|
+
endsWithFunction,
|
|
73
|
+
upperFunction,
|
|
74
|
+
lowerFunction,
|
|
75
|
+
indexOfFunction,
|
|
76
|
+
replaceFunction,
|
|
77
|
+
splitFunction,
|
|
78
|
+
joinFunction,
|
|
79
|
+
trimFunction,
|
|
80
|
+
toCharsFunction,
|
|
81
|
+
toStringFunction,
|
|
82
|
+
toIntegerFunction,
|
|
83
|
+
toDecimalFunction,
|
|
84
|
+
toBooleanFunction,
|
|
85
|
+
toQuantityFunction,
|
|
86
|
+
tailFunction,
|
|
87
|
+
skipFunction,
|
|
88
|
+
takeFunction,
|
|
89
|
+
unionFunction,
|
|
90
|
+
combineFunction,
|
|
91
|
+
intersectFunction,
|
|
92
|
+
excludeFunction
|
|
93
|
+
].forEach(op => Registry.register(op));
|
|
94
|
+
|
|
95
|
+
// Export default registry instance
|
|
96
|
+
export default Registry;
|