@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,110 @@
|
|
|
1
|
+
import type { Token } from '../lexer/token';
|
|
2
|
+
import { TokenType } from '../lexer/token';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles token navigation and lookahead operations for the parser
|
|
6
|
+
*/
|
|
7
|
+
export class TokenNavigator {
|
|
8
|
+
private tokens: Token[];
|
|
9
|
+
private current: number = 0;
|
|
10
|
+
|
|
11
|
+
constructor(tokens: Token[]) {
|
|
12
|
+
this.tokens = tokens;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if the current token matches any of the given types
|
|
17
|
+
*/
|
|
18
|
+
match(...types: TokenType[]): boolean {
|
|
19
|
+
for (const type of types) {
|
|
20
|
+
if (this.check(type)) {
|
|
21
|
+
this.advance();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if the current token is of the given type
|
|
30
|
+
*/
|
|
31
|
+
check(type: TokenType): boolean {
|
|
32
|
+
if (this.isAtEnd()) return false;
|
|
33
|
+
return this.peek().type === type;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Advance to the next token and return the previous one
|
|
38
|
+
*/
|
|
39
|
+
advance(): Token {
|
|
40
|
+
if (!this.isAtEnd()) this.current++;
|
|
41
|
+
return this.previous();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if we've reached the end of tokens
|
|
46
|
+
*/
|
|
47
|
+
isAtEnd(): boolean {
|
|
48
|
+
return this.peek().type === TokenType.EOF;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current token without advancing
|
|
53
|
+
*/
|
|
54
|
+
peek(): Token {
|
|
55
|
+
if (this.tokens.length === 0) {
|
|
56
|
+
// Return a synthetic EOF token when there are no tokens
|
|
57
|
+
return {
|
|
58
|
+
type: TokenType.EOF,
|
|
59
|
+
value: '',
|
|
60
|
+
position: { line: 1, column: 1, offset: 0 }
|
|
61
|
+
} as Token;
|
|
62
|
+
}
|
|
63
|
+
return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the previous token
|
|
68
|
+
*/
|
|
69
|
+
previous(): Token {
|
|
70
|
+
return this.tokens[this.current - 1]!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get current position in token stream
|
|
75
|
+
*/
|
|
76
|
+
getCurrentPosition(): number {
|
|
77
|
+
return this.current;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Restore position in token stream
|
|
82
|
+
*/
|
|
83
|
+
restorePosition(position: number): void {
|
|
84
|
+
this.current = position;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Consume a token of the expected type or throw an error
|
|
89
|
+
*/
|
|
90
|
+
consume(type: TokenType, errorCallback: (token: Token) => Error): Token {
|
|
91
|
+
if (this.check(type)) return this.advance();
|
|
92
|
+
throw errorCallback(this.peek());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Skip tokens while a condition is true
|
|
97
|
+
*/
|
|
98
|
+
skipWhile(condition: (token: Token) => boolean): void {
|
|
99
|
+
while (!this.isAtEnd() && condition(this.peek())) {
|
|
100
|
+
this.advance();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if any of the token types match current token
|
|
106
|
+
*/
|
|
107
|
+
checkAny(types: TokenType[]): boolean {
|
|
108
|
+
return types.some(type => this.check(type));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ASTNode } from './ast';
|
|
2
|
+
import type { ErrorCode } from '../api/errors';
|
|
3
|
+
|
|
4
|
+
export interface ParserOptions {
|
|
5
|
+
maxErrors?: number;
|
|
6
|
+
throwOnError?: boolean; // When true, throws on first error instead of collecting diagnostics
|
|
7
|
+
trackRanges?: boolean; // Enable source range tracking for each AST node (useful for IDEs)
|
|
8
|
+
errorRecovery?: boolean; // Enable error recovery to continue parsing after errors (useful for IDEs)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ParseResult {
|
|
12
|
+
ast: ASTNode;
|
|
13
|
+
diagnostics: ParseDiagnostic[];
|
|
14
|
+
hasErrors: boolean;
|
|
15
|
+
isPartial?: boolean; // Present when errorRecovery is enabled
|
|
16
|
+
ranges?: Map<ASTNode, TextRange>; // Present when trackRanges is enabled
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ParseDiagnostic {
|
|
20
|
+
range: TextRange;
|
|
21
|
+
severity: DiagnosticSeverity;
|
|
22
|
+
code: ErrorCode;
|
|
23
|
+
message: string;
|
|
24
|
+
source: 'fhirpath-parser';
|
|
25
|
+
relatedInformation?: RelatedInformation[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export enum DiagnosticSeverity {
|
|
29
|
+
Error = 1,
|
|
30
|
+
Warning = 2,
|
|
31
|
+
Information = 3,
|
|
32
|
+
Hint = 4
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TextRange {
|
|
36
|
+
start: Position;
|
|
37
|
+
end: Position;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Position {
|
|
41
|
+
line: number;
|
|
42
|
+
character: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RelatedInformation {
|
|
47
|
+
location: TextRange;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export enum ParseContext {
|
|
52
|
+
Expression,
|
|
53
|
+
FunctionCall,
|
|
54
|
+
IndexExpression,
|
|
55
|
+
BinaryExpression,
|
|
56
|
+
UnaryExpression,
|
|
57
|
+
CollectionLiteral,
|
|
58
|
+
TypeCast,
|
|
59
|
+
MembershipTest
|
|
60
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# FHIRPath Parser2 - Recursive Descent with Pratt Parsing
|
|
2
|
+
|
|
3
|
+
This parser implements a recursive-descent parser with Pratt operator precedence parsing for FHIRPath expressions. It's designed to be simple, efficient, and self-contained.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
The parser consists of:
|
|
8
|
+
- **Lexical Analysis**: Uses `lexer2` to tokenize the input
|
|
9
|
+
- **Recursive Descent**: Top-down parsing for primary expressions
|
|
10
|
+
- **Pratt Parsing**: Handles operator precedence and associativity
|
|
11
|
+
- **AST Construction**: Builds a typed Abstract Syntax Tree
|
|
12
|
+
|
|
13
|
+
## Parser Flow
|
|
14
|
+
|
|
15
|
+
### 1. Initialization
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const parser = new Parser(input);
|
|
19
|
+
// 1. Creates a Lexer instance
|
|
20
|
+
// 2. Tokenizes entire input upfront
|
|
21
|
+
// 3. Stores tokens in array for random access
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Main Entry Point
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
parse(): ASTNode
|
|
28
|
+
```
|
|
29
|
+
- Calls `expression()` to parse the entire input
|
|
30
|
+
- Ensures all tokens are consumed (no trailing tokens)
|
|
31
|
+
- Returns the root AST node
|
|
32
|
+
|
|
33
|
+
### 3. Expression Parsing (Pratt Algorithm)
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
parseExpressionWithPrecedence(minPrecedence): ASTNode
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The core of the parser uses Pratt parsing:
|
|
40
|
+
|
|
41
|
+
1. **Parse Primary Expression** - Get the left-hand side
|
|
42
|
+
2. **Parse Operators Loop** - While we have operators with precedence >= minPrecedence:
|
|
43
|
+
- Consume the operator
|
|
44
|
+
- Parse right-hand side with appropriate precedence
|
|
45
|
+
- Create binary/special nodes
|
|
46
|
+
|
|
47
|
+
#### Operator Precedence (highest to lowest):
|
|
48
|
+
|
|
49
|
+
| Precedence | Operators | Associativity |
|
|
50
|
+
|------------|-----------|---------------|
|
|
51
|
+
| 100 | `.` (member), `[` (index), `(` (call) | Left |
|
|
52
|
+
| 90 | `is`, `as` | Left |
|
|
53
|
+
| 80 | `*`, `/`, `div`, `mod` | Left |
|
|
54
|
+
| 70 | `+`, `-` | Left |
|
|
55
|
+
| 60 | `&` | Left |
|
|
56
|
+
| 50 | `<`, `>`, `<=`, `>=` | Left |
|
|
57
|
+
| 40 | `=`, `!=`, `~`, `!~` | Left |
|
|
58
|
+
| 35 | `in`, `contains` | Left |
|
|
59
|
+
| 30 | `and` | Left |
|
|
60
|
+
| 20 | `or`, `xor` | Left |
|
|
61
|
+
| 10 | `implies` | Right |
|
|
62
|
+
| 5 | `|` (union) | Left |
|
|
63
|
+
|
|
64
|
+
### 4. Primary Expression Parsing
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
parsePrimary(): ASTNode
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Handles atomic expressions:
|
|
71
|
+
- **Literals**: Numbers, strings, booleans, null, datetime, time
|
|
72
|
+
- **Variables**: `$this`, `$index`, `$total`, `%env`
|
|
73
|
+
- **Identifiers**: Simple or delimited (backtick) identifiers
|
|
74
|
+
- **Parentheses**: `(expression)`
|
|
75
|
+
- **Collections**: `{element1, element2, ...}`
|
|
76
|
+
- **Unary Operators**: `+expr`, `-expr`
|
|
77
|
+
|
|
78
|
+
### 5. Special Constructs
|
|
79
|
+
|
|
80
|
+
#### Member Access and Function Calls
|
|
81
|
+
|
|
82
|
+
After parsing `.identifier`, the parser checks if it's followed by `(` to distinguish between:
|
|
83
|
+
- Property access: `Patient.name`
|
|
84
|
+
- Method call: `Patient.name.where(...)`
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
parseInvocation(): ASTNode
|
|
88
|
+
// 1. Parse identifier after dot
|
|
89
|
+
// 2. Check for '(' to determine if it's a function call
|
|
90
|
+
// 3. Return appropriate node type
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Type Operations
|
|
94
|
+
|
|
95
|
+
- **Type Test**: `expression is TypeName`
|
|
96
|
+
- **Type Cast**: `expression as TypeName`
|
|
97
|
+
|
|
98
|
+
Both create special node types rather than generic binary operators.
|
|
99
|
+
|
|
100
|
+
#### Union Operator
|
|
101
|
+
|
|
102
|
+
The `|` operator is special - it creates/extends a UnionNode with multiple operands:
|
|
103
|
+
```typescript
|
|
104
|
+
a | b | c => UnionNode { operands: [a, b, c] }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 6. AST Node Types
|
|
108
|
+
|
|
109
|
+
All AST nodes implement the base `ASTNode` interface:
|
|
110
|
+
```typescript
|
|
111
|
+
interface ASTNode {
|
|
112
|
+
type: NodeType;
|
|
113
|
+
position: Position;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Node types include:
|
|
118
|
+
- **Identifier**: Simple identifiers like `name`
|
|
119
|
+
- **TypeOrIdentifier**: Uppercase identifiers like `Patient`
|
|
120
|
+
- **Literal**: Values with type information
|
|
121
|
+
- **Binary**: Binary operators with left/right operands
|
|
122
|
+
- **Unary**: Unary operators with single operand
|
|
123
|
+
- **Function**: Function calls with name and arguments
|
|
124
|
+
- **Variable**: Special variables (`$this`, `%env`)
|
|
125
|
+
- **Index**: Array/collection indexing
|
|
126
|
+
- **Union**: Multiple operands joined by `|`
|
|
127
|
+
- **MembershipTest**: `is` operator
|
|
128
|
+
- **TypeCast**: `as` operator
|
|
129
|
+
- **Collection**: `{}` expressions
|
|
130
|
+
|
|
131
|
+
## Example Parse Flow
|
|
132
|
+
|
|
133
|
+
For expression: `Patient.name.where(use = 'official').given`
|
|
134
|
+
|
|
135
|
+
1. **Primary**: Parse `Patient` → TypeOrIdentifierNode
|
|
136
|
+
2. **Dot operator** (precedence 100):
|
|
137
|
+
- Parse `name` → IdentifierNode
|
|
138
|
+
- Create BinaryNode(DOT, Patient, name)
|
|
139
|
+
3. **Dot operator**:
|
|
140
|
+
- Parse `where` → IdentifierNode
|
|
141
|
+
- Check for `(` → It's a function call!
|
|
142
|
+
- Parse arguments: `use = 'official'`
|
|
143
|
+
- Create FunctionNode(where, [BinaryNode(EQ, use, 'official')])
|
|
144
|
+
- Create BinaryNode(DOT, Patient.name, where(...))
|
|
145
|
+
4. **Dot operator**:
|
|
146
|
+
- Parse `given` → IdentifierNode
|
|
147
|
+
- Create BinaryNode(DOT, Patient.name.where(...), given)
|
|
148
|
+
|
|
149
|
+
## Key Design Decisions
|
|
150
|
+
|
|
151
|
+
1. **Tokenize Upfront**: All tokens are generated before parsing begins, allowing lookahead and backtracking if needed.
|
|
152
|
+
|
|
153
|
+
2. **Pratt Parsing**: Eliminates the need for separate grammar rules for each precedence level, making the parser more maintainable.
|
|
154
|
+
|
|
155
|
+
3. **Special Node Types**: Instead of generic nodes, specific types like `MembershipTest` and `TypeCast` preserve semantic information.
|
|
156
|
+
|
|
157
|
+
4. **Self-Contained**: All types are defined within the module, making it independent and easy to understand.
|
|
158
|
+
|
|
159
|
+
5. **Position Tracking**: Every node includes position information for error reporting and tooling support.
|
|
160
|
+
|
|
161
|
+
## Usage
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { parse } from './parser2';
|
|
165
|
+
|
|
166
|
+
const ast = parse('Patient.name.given');
|
|
167
|
+
console.log(JSON.stringify(ast, null, 2));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Error Handling
|
|
171
|
+
|
|
172
|
+
The parser throws descriptive errors for:
|
|
173
|
+
- Unexpected tokens
|
|
174
|
+
- Missing closing delimiters
|
|
175
|
+
- Invalid syntax constructs
|
|
176
|
+
|
|
177
|
+
Errors include the problematic token value for debugging.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it } from 'bun:test';
|
|
2
|
+
import { Parser } from './index';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('Parser2 Performance', () => {
|
|
7
|
+
it('measures parser performance on fixture expressions', () => {
|
|
8
|
+
runPerformanceTest();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function runPerformanceTest() {
|
|
13
|
+
const fixturesPath = path.join(process.cwd(), 'test', 'fixtures');
|
|
14
|
+
const iterations = 5000; // Fewer iterations than lexer since parsing is more expensive
|
|
15
|
+
|
|
16
|
+
// Read all fixture files
|
|
17
|
+
const fixtureFiles = fs.readdirSync(fixturesPath)
|
|
18
|
+
.filter(file => file.endsWith('.json'))
|
|
19
|
+
.map(file => ({
|
|
20
|
+
name: file,
|
|
21
|
+
path: path.join(fixturesPath, file)
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
console.log(`\nRunning parser2 performance tests with ${iterations} iterations per expression\n`);
|
|
25
|
+
|
|
26
|
+
let totalExpressions = 0;
|
|
27
|
+
let totalIterations = 0;
|
|
28
|
+
let totalTime = 0;
|
|
29
|
+
let totalTokens = 0;
|
|
30
|
+
let totalNodes = 0;
|
|
31
|
+
const expressionStats: { expression: string; time: number; tokens: number; nodes: number }[] = [];
|
|
32
|
+
|
|
33
|
+
for (const fixture of fixtureFiles) {
|
|
34
|
+
console.log(`Processing ${fixture.name}...`);
|
|
35
|
+
|
|
36
|
+
const content = fs.readFileSync(fixture.path, 'utf-8');
|
|
37
|
+
const expressions: string[] = JSON.parse(content);
|
|
38
|
+
|
|
39
|
+
for (const expression of expressions) {
|
|
40
|
+
if (!expression) continue;
|
|
41
|
+
|
|
42
|
+
// Warm up run and get stats
|
|
43
|
+
const warmupParser = new Parser(expression);
|
|
44
|
+
const ast = warmupParser.parse();
|
|
45
|
+
const tokenCount = countTokens(warmupParser);
|
|
46
|
+
const nodeCount = countNodes(ast);
|
|
47
|
+
|
|
48
|
+
// Measure total time for all iterations
|
|
49
|
+
const start = performance.now();
|
|
50
|
+
for (let j = 0; j < iterations; j++) {
|
|
51
|
+
const parser = new Parser(expression);
|
|
52
|
+
parser.parse();
|
|
53
|
+
}
|
|
54
|
+
const end = performance.now();
|
|
55
|
+
|
|
56
|
+
const totalTimeForExpression = end - start;
|
|
57
|
+
totalTime += totalTimeForExpression;
|
|
58
|
+
totalExpressions++;
|
|
59
|
+
totalIterations += iterations;
|
|
60
|
+
totalTokens += tokenCount * iterations;
|
|
61
|
+
totalNodes += nodeCount * iterations;
|
|
62
|
+
|
|
63
|
+
expressionStats.push({
|
|
64
|
+
expression,
|
|
65
|
+
time: totalTimeForExpression / iterations,
|
|
66
|
+
tokens: tokenCount,
|
|
67
|
+
nodes: nodeCount
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const avgTimePerExpression = totalTime / totalIterations;
|
|
73
|
+
const avgTokensPerExpression = totalTokens / totalIterations;
|
|
74
|
+
const avgNodesPerExpression = totalNodes / totalIterations;
|
|
75
|
+
|
|
76
|
+
// Sort by time to find slowest expressions
|
|
77
|
+
expressionStats.sort((a, b) => b.time - a.time);
|
|
78
|
+
|
|
79
|
+
console.log('\n' + '='.repeat(70));
|
|
80
|
+
console.log('PARSER2 PERFORMANCE RESULTS');
|
|
81
|
+
console.log('='.repeat(70));
|
|
82
|
+
console.log(`Total expressions: ${totalExpressions}`);
|
|
83
|
+
console.log(`Total iterations: ${totalIterations}`);
|
|
84
|
+
console.log(`Total time: ${(totalTime / 1000).toFixed(2)}s`);
|
|
85
|
+
console.log(`Time per expression: ${avgTimePerExpression.toFixed(4)}ms`);
|
|
86
|
+
console.log(`Expressions per second: ${(1000 / avgTimePerExpression).toFixed(0)}`);
|
|
87
|
+
console.log(`Average tokens per expression: ${avgTokensPerExpression.toFixed(1)}`);
|
|
88
|
+
console.log(`Average AST nodes per expression: ${avgNodesPerExpression.toFixed(1)}`);
|
|
89
|
+
|
|
90
|
+
console.log('\n' + '='.repeat(70));
|
|
91
|
+
console.log('TOP 10 SLOWEST EXPRESSIONS');
|
|
92
|
+
console.log('='.repeat(70));
|
|
93
|
+
console.log('Time (ms) | Tokens | Nodes | Expression');
|
|
94
|
+
console.log('-'.repeat(70));
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < Math.min(10, expressionStats.length); i++) {
|
|
97
|
+
const stat = expressionStats[i];
|
|
98
|
+
const expr = stat.expression.length > 40
|
|
99
|
+
? stat.expression.substring(0, 37) + '...'
|
|
100
|
+
: stat.expression;
|
|
101
|
+
console.log(
|
|
102
|
+
`${stat.time.toFixed(4).padStart(9)} | ` +
|
|
103
|
+
`${stat.tokens.toString().padStart(6)} | ` +
|
|
104
|
+
`${stat.nodes.toString().padStart(5)} | ` +
|
|
105
|
+
expr
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Calculate complexity metrics
|
|
110
|
+
const complexityStats = expressionStats.map(stat => ({
|
|
111
|
+
...stat,
|
|
112
|
+
timePerToken: stat.time / stat.tokens,
|
|
113
|
+
timePerNode: stat.time / stat.nodes
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
console.log('\n' + '='.repeat(70));
|
|
117
|
+
console.log('COMPLEXITY ANALYSIS');
|
|
118
|
+
console.log('='.repeat(70));
|
|
119
|
+
|
|
120
|
+
// Group by token count ranges
|
|
121
|
+
const tokenRanges = [
|
|
122
|
+
{ min: 0, max: 5, label: '1-5 tokens' },
|
|
123
|
+
{ min: 5, max: 10, label: '6-10 tokens' },
|
|
124
|
+
{ min: 10, max: 20, label: '11-20 tokens' },
|
|
125
|
+
{ min: 20, max: 50, label: '21-50 tokens' },
|
|
126
|
+
{ min: 50, max: Infinity, label: '50+ tokens' }
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
console.log('\nPerformance by expression complexity:');
|
|
130
|
+
console.log('Token Range | Count | Avg Time (ms) | Time/Token (μs)');
|
|
131
|
+
console.log('-'.repeat(55));
|
|
132
|
+
|
|
133
|
+
for (const range of tokenRanges) {
|
|
134
|
+
const inRange = complexityStats.filter(
|
|
135
|
+
s => s.tokens > range.min && s.tokens <= range.max
|
|
136
|
+
);
|
|
137
|
+
if (inRange.length > 0) {
|
|
138
|
+
const avgTime = inRange.reduce((sum, s) => sum + s.time, 0) / inRange.length;
|
|
139
|
+
const avgTimePerToken = inRange.reduce((sum, s) => sum + s.timePerToken, 0) / inRange.length;
|
|
140
|
+
console.log(
|
|
141
|
+
`${range.label.padEnd(12)} | ${inRange.length.toString().padStart(5)} | ` +
|
|
142
|
+
`${avgTime.toFixed(4).padStart(13)} | ${(avgTimePerToken * 1000).toFixed(2).padStart(15)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function countTokens(parser: Parser): number {
|
|
149
|
+
// Access the private tokens array via reflection
|
|
150
|
+
return (parser as any).tokens.length;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function countNodes(node: any): number {
|
|
154
|
+
if (!node) return 0;
|
|
155
|
+
|
|
156
|
+
let count = 1;
|
|
157
|
+
|
|
158
|
+
// Count nodes in common structures
|
|
159
|
+
if (node.left) count += countNodes(node.left);
|
|
160
|
+
if (node.right) count += countNodes(node.right);
|
|
161
|
+
if (node.operand) count += countNodes(node.operand);
|
|
162
|
+
if (node.expression) count += countNodes(node.expression);
|
|
163
|
+
if (node.index) count += countNodes(node.index);
|
|
164
|
+
if (node.name && typeof node.name === 'object') count += countNodes(node.name);
|
|
165
|
+
|
|
166
|
+
// Count nodes in arrays
|
|
167
|
+
if (node.arguments) {
|
|
168
|
+
for (const arg of node.arguments) {
|
|
169
|
+
count += countNodes(arg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.elements) {
|
|
173
|
+
for (const elem of node.elements) {
|
|
174
|
+
count += countNodes(elem);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (node.operands) {
|
|
178
|
+
for (const op of node.operands) {
|
|
179
|
+
count += countNodes(op);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return count;
|
|
184
|
+
}
|