@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,85 @@
|
|
|
1
|
+
import type { ParseDiagnostic, DiagnosticSeverity, TextRange } from './types';
|
|
2
|
+
import type { ErrorCode } from '../api/errors';
|
|
3
|
+
|
|
4
|
+
export class DiagnosticCollector {
|
|
5
|
+
private diagnostics: ParseDiagnostic[] = [];
|
|
6
|
+
private errorCount = 0;
|
|
7
|
+
private warningCount = 0;
|
|
8
|
+
private maxErrors: number;
|
|
9
|
+
|
|
10
|
+
constructor(maxErrors: number = Number.MAX_SAFE_INTEGER) {
|
|
11
|
+
this.maxErrors = maxErrors;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addError(range: TextRange, message: string, code: ErrorCode): void {
|
|
15
|
+
if (this.errorCount >= this.maxErrors) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.diagnostics.push({
|
|
20
|
+
range,
|
|
21
|
+
severity: 1 as DiagnosticSeverity.Error,
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
source: 'fhirpath-parser'
|
|
25
|
+
});
|
|
26
|
+
this.errorCount++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addWarning(range: TextRange, message: string, code: ErrorCode): void {
|
|
30
|
+
this.diagnostics.push({
|
|
31
|
+
range,
|
|
32
|
+
severity: 2 as DiagnosticSeverity.Warning,
|
|
33
|
+
code,
|
|
34
|
+
message,
|
|
35
|
+
source: 'fhirpath-parser'
|
|
36
|
+
});
|
|
37
|
+
this.warningCount++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addInfo(range: TextRange, message: string, code: ErrorCode): void {
|
|
41
|
+
this.diagnostics.push({
|
|
42
|
+
range,
|
|
43
|
+
severity: 3 as DiagnosticSeverity.Information,
|
|
44
|
+
code,
|
|
45
|
+
message,
|
|
46
|
+
source: 'fhirpath-parser'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addHint(range: TextRange, message: string, code: ErrorCode): void {
|
|
51
|
+
this.diagnostics.push({
|
|
52
|
+
range,
|
|
53
|
+
severity: 4 as DiagnosticSeverity.Hint,
|
|
54
|
+
code,
|
|
55
|
+
message,
|
|
56
|
+
source: 'fhirpath-parser'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getDiagnostics(): ParseDiagnostic[] {
|
|
61
|
+
return [...this.diagnostics];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
hasErrors(): boolean {
|
|
65
|
+
return this.errorCount > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
hasWarnings(): boolean {
|
|
69
|
+
return this.warningCount > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getErrorCount(): number {
|
|
73
|
+
return this.errorCount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getWarningCount(): number {
|
|
77
|
+
return this.warningCount;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clear(): void {
|
|
81
|
+
this.diagnostics = [];
|
|
82
|
+
this.errorCount = 0;
|
|
83
|
+
this.warningCount = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { TokenType, type Token } from '../lexer/token';
|
|
2
|
+
import type { SourceMapper } from './source-mapper';
|
|
3
|
+
import type { DiagnosticCollector } from './diagnostics';
|
|
4
|
+
import { ErrorCode } from '../api/errors';
|
|
5
|
+
|
|
6
|
+
export enum ParseContext {
|
|
7
|
+
Expression = 'Expression',
|
|
8
|
+
FunctionCall = 'FunctionCall',
|
|
9
|
+
IndexExpression = 'IndexExpression',
|
|
10
|
+
BinaryExpression = 'BinaryExpression',
|
|
11
|
+
UnaryExpression = 'UnaryExpression',
|
|
12
|
+
CollectionLiteral = 'CollectionLiteral',
|
|
13
|
+
TypeCast = 'TypeCast',
|
|
14
|
+
MembershipTest = 'MembershipTest'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ContextualErrorReporter {
|
|
18
|
+
constructor(
|
|
19
|
+
private sourceMapper: SourceMapper,
|
|
20
|
+
private collector: DiagnosticCollector
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
reportExpectedToken(
|
|
24
|
+
expected: TokenType[],
|
|
25
|
+
actual: Token,
|
|
26
|
+
context: ParseContext
|
|
27
|
+
): void {
|
|
28
|
+
const message = this.buildContextualMessage(expected, actual, context);
|
|
29
|
+
const range = this.sourceMapper.tokenToRange(actual);
|
|
30
|
+
|
|
31
|
+
this.collector.addError(range, message, ErrorCode.UNEXPECTED_TOKEN);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
reportUnclosedDelimiter(openToken: Token, expectedClose: string): void {
|
|
35
|
+
const range = this.sourceMapper.tokenToRange(openToken);
|
|
36
|
+
let message: string;
|
|
37
|
+
let code: ErrorCode;
|
|
38
|
+
|
|
39
|
+
switch (expectedClose) {
|
|
40
|
+
case ')':
|
|
41
|
+
code = ErrorCode.UNCLOSED_PARENTHESIS;
|
|
42
|
+
message = "Unclosed parenthesis - missing ')' to close function call";
|
|
43
|
+
break;
|
|
44
|
+
case ']':
|
|
45
|
+
code = ErrorCode.UNCLOSED_BRACKET;
|
|
46
|
+
message = "Expected ']' after index expression";
|
|
47
|
+
break;
|
|
48
|
+
case '}':
|
|
49
|
+
code = ErrorCode.UNCLOSED_BRACE;
|
|
50
|
+
message = "Expected '}' to close collection literal";
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
code = ErrorCode.SYNTAX_ERROR;
|
|
54
|
+
message = `Unclosed delimiter - missing '${expectedClose}' to close '${openToken.value}'`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.collector.addError(range, message, code);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reportInvalidOperator(operator: Token, message: string): void {
|
|
61
|
+
const range = this.sourceMapper.tokenToRange(operator);
|
|
62
|
+
this.collector.addError(range, message, ErrorCode.INVALID_OPERATOR);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reportMissingExpression(token: Token, context: ParseContext): void {
|
|
66
|
+
const range = this.sourceMapper.tokenToRange(token);
|
|
67
|
+
const contextDesc = this.getContextDescription(context);
|
|
68
|
+
const message = `Expected expression ${contextDesc}`;
|
|
69
|
+
|
|
70
|
+
this.collector.addError(range, message, ErrorCode.EXPECTED_EXPRESSION);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
reportMissingIdentifier(token: Token, context: string): void {
|
|
74
|
+
const range = this.sourceMapper.tokenToRange(token);
|
|
75
|
+
const message = `Expected identifier ${context}`;
|
|
76
|
+
|
|
77
|
+
this.collector.addError(range, message, ErrorCode.EXPECTED_IDENTIFIER);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reportMissingArguments(functionName: string, token: Token): void {
|
|
81
|
+
const range = this.sourceMapper.tokenToRange(token);
|
|
82
|
+
const message = `Function '${functionName}' requires arguments`;
|
|
83
|
+
|
|
84
|
+
this.collector.addError(range, message, ErrorCode.MISSING_ARGUMENTS);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private buildContextualMessage(
|
|
88
|
+
expected: TokenType[],
|
|
89
|
+
actual: Token,
|
|
90
|
+
context: ParseContext
|
|
91
|
+
): string {
|
|
92
|
+
const expectedDesc = this.describeTokens(expected);
|
|
93
|
+
const actualDesc = this.describeToken(actual);
|
|
94
|
+
|
|
95
|
+
switch (context) {
|
|
96
|
+
case ParseContext.FunctionCall:
|
|
97
|
+
return `Expected ${expectedDesc} in function call, found ${actualDesc}`;
|
|
98
|
+
case ParseContext.IndexExpression:
|
|
99
|
+
return `Expected ${expectedDesc} in index expression, found ${actualDesc}`;
|
|
100
|
+
case ParseContext.BinaryExpression:
|
|
101
|
+
return `Expected ${expectedDesc} after operator, found ${actualDesc}`;
|
|
102
|
+
case ParseContext.CollectionLiteral:
|
|
103
|
+
return `Expected ${expectedDesc} in collection literal, found ${actualDesc}`;
|
|
104
|
+
case ParseContext.TypeCast:
|
|
105
|
+
return `Expected ${expectedDesc} after 'as', found ${actualDesc}`;
|
|
106
|
+
case ParseContext.MembershipTest:
|
|
107
|
+
return `Expected ${expectedDesc} after 'is', found ${actualDesc}`;
|
|
108
|
+
default:
|
|
109
|
+
return `Expected ${expectedDesc}, found ${actualDesc}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getContextDescription(context: ParseContext): string {
|
|
114
|
+
switch (context) {
|
|
115
|
+
case ParseContext.FunctionCall:
|
|
116
|
+
return "in function call";
|
|
117
|
+
case ParseContext.IndexExpression:
|
|
118
|
+
return "in index expression";
|
|
119
|
+
case ParseContext.BinaryExpression:
|
|
120
|
+
return "after operator";
|
|
121
|
+
case ParseContext.CollectionLiteral:
|
|
122
|
+
return "in collection literal";
|
|
123
|
+
case ParseContext.TypeCast:
|
|
124
|
+
return "after 'as'";
|
|
125
|
+
case ParseContext.MembershipTest:
|
|
126
|
+
return "after 'is'";
|
|
127
|
+
default:
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private describeTokens(tokens: TokenType[]): string {
|
|
133
|
+
if (tokens.length === 0) return "token";
|
|
134
|
+
if (tokens.length === 1) return this.getTokenDescription(tokens[0]!);
|
|
135
|
+
|
|
136
|
+
const descriptions = tokens.map(t => this.getTokenDescription(t));
|
|
137
|
+
const last = descriptions.pop();
|
|
138
|
+
return descriptions.join(', ') + ' or ' + last;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private describeToken(token: Token): string {
|
|
142
|
+
if (token.type === TokenType.IDENTIFIER) {
|
|
143
|
+
return `identifier '${token.value}'`;
|
|
144
|
+
}
|
|
145
|
+
if (token.type === TokenType.NUMBER || token.type === TokenType.STRING) {
|
|
146
|
+
return `${this.getTokenDescription(token.type)} '${token.value}'`;
|
|
147
|
+
}
|
|
148
|
+
if (token.type === TokenType.EOF) {
|
|
149
|
+
return "end of expression";
|
|
150
|
+
}
|
|
151
|
+
return `'${token.value}'`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private getTokenDescription(tokenType: TokenType): string {
|
|
155
|
+
switch (tokenType) {
|
|
156
|
+
case TokenType.IDENTIFIER:
|
|
157
|
+
return "identifier";
|
|
158
|
+
case TokenType.NUMBER:
|
|
159
|
+
return "number";
|
|
160
|
+
case TokenType.STRING:
|
|
161
|
+
return "string";
|
|
162
|
+
case TokenType.LPAREN:
|
|
163
|
+
return "'('";
|
|
164
|
+
case TokenType.RPAREN:
|
|
165
|
+
return "')'";
|
|
166
|
+
case TokenType.LBRACKET:
|
|
167
|
+
return "'['";
|
|
168
|
+
case TokenType.RBRACKET:
|
|
169
|
+
return "']'";
|
|
170
|
+
case TokenType.LBRACE:
|
|
171
|
+
return "'{'";
|
|
172
|
+
case TokenType.RBRACE:
|
|
173
|
+
return "'}'";
|
|
174
|
+
case TokenType.COMMA:
|
|
175
|
+
return "','";
|
|
176
|
+
case TokenType.DOT:
|
|
177
|
+
return "'.'";
|
|
178
|
+
case TokenType.PLUS:
|
|
179
|
+
return "'+'";
|
|
180
|
+
case TokenType.MINUS:
|
|
181
|
+
return "'-'";
|
|
182
|
+
case TokenType.STAR:
|
|
183
|
+
return "'*'";
|
|
184
|
+
case TokenType.SLASH:
|
|
185
|
+
return "'/'";
|
|
186
|
+
case TokenType.MOD:
|
|
187
|
+
return "'mod'";
|
|
188
|
+
case TokenType.EQ:
|
|
189
|
+
return "'='";
|
|
190
|
+
case TokenType.NEQ:
|
|
191
|
+
return "'!='";
|
|
192
|
+
case TokenType.LT:
|
|
193
|
+
return "'<'";
|
|
194
|
+
case TokenType.GT:
|
|
195
|
+
return "'>'";
|
|
196
|
+
case TokenType.LTE:
|
|
197
|
+
return "'<='";
|
|
198
|
+
case TokenType.GTE:
|
|
199
|
+
return "'>='";
|
|
200
|
+
case TokenType.AND:
|
|
201
|
+
return "'and'";
|
|
202
|
+
case TokenType.OR:
|
|
203
|
+
return "'or'";
|
|
204
|
+
case TokenType.XOR:
|
|
205
|
+
return "'xor'";
|
|
206
|
+
case TokenType.IMPLIES:
|
|
207
|
+
return "'implies'";
|
|
208
|
+
case TokenType.NOT:
|
|
209
|
+
return "'not'";
|
|
210
|
+
case TokenType.IS:
|
|
211
|
+
return "'is'";
|
|
212
|
+
case TokenType.AS:
|
|
213
|
+
return "'as'";
|
|
214
|
+
case TokenType.IN:
|
|
215
|
+
return "'in'";
|
|
216
|
+
case TokenType.CONTAINS:
|
|
217
|
+
return "'contains'";
|
|
218
|
+
case TokenType.TRUE:
|
|
219
|
+
return "'true'";
|
|
220
|
+
case TokenType.FALSE:
|
|
221
|
+
return "'false'";
|
|
222
|
+
case TokenType.NULL:
|
|
223
|
+
return "'null'";
|
|
224
|
+
case TokenType.EOF:
|
|
225
|
+
return "end of expression";
|
|
226
|
+
default:
|
|
227
|
+
return "token";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Token } from '../lexer/token';
|
|
2
|
+
import { TokenType } from '../lexer/token';
|
|
3
|
+
import type { ASTNode, LiteralNode, VariableNode } from './ast';
|
|
4
|
+
import { ASTFactory } from './ast-factory';
|
|
5
|
+
import type { TokenNavigator } from './token-navigator';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handles parsing of literals and variables
|
|
9
|
+
*/
|
|
10
|
+
export class LiteralParser {
|
|
11
|
+
/**
|
|
12
|
+
* Parse all types of literals
|
|
13
|
+
*/
|
|
14
|
+
static parseLiteral(navigator: TokenNavigator): ASTNode | null {
|
|
15
|
+
// Handle registry-based literals
|
|
16
|
+
if (navigator.match(TokenType.LITERAL)) {
|
|
17
|
+
const token = navigator.previous();
|
|
18
|
+
return ASTFactory.createLiteral(
|
|
19
|
+
token.literalValue ?? token.value,
|
|
20
|
+
ASTFactory.inferLiteralType(token.literalValue ?? token.value),
|
|
21
|
+
token.position,
|
|
22
|
+
token.value,
|
|
23
|
+
token.operation
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle legacy literals
|
|
28
|
+
if (navigator.match(TokenType.NUMBER)) {
|
|
29
|
+
const token = navigator.previous();
|
|
30
|
+
return ASTFactory.createLiteral(
|
|
31
|
+
parseFloat(token.value),
|
|
32
|
+
'number',
|
|
33
|
+
token.position
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (navigator.match(TokenType.STRING)) {
|
|
38
|
+
const token = navigator.previous();
|
|
39
|
+
return ASTFactory.createLiteral(
|
|
40
|
+
token.value,
|
|
41
|
+
'string',
|
|
42
|
+
token.position
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (navigator.match(TokenType.TRUE, TokenType.FALSE)) {
|
|
47
|
+
const token = navigator.previous();
|
|
48
|
+
return ASTFactory.createLiteral(
|
|
49
|
+
token.type === TokenType.TRUE,
|
|
50
|
+
'boolean',
|
|
51
|
+
token.position
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (navigator.match(TokenType.NULL)) {
|
|
56
|
+
const token = navigator.previous();
|
|
57
|
+
return ASTFactory.createLiteral(
|
|
58
|
+
null,
|
|
59
|
+
'null',
|
|
60
|
+
token.position
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Date/time literals
|
|
65
|
+
if (navigator.match(TokenType.DATE, TokenType.TIME, TokenType.DATETIME)) {
|
|
66
|
+
const token = navigator.previous();
|
|
67
|
+
return ASTFactory.createLiteral(
|
|
68
|
+
token.value,
|
|
69
|
+
token.type === TokenType.DATE ? 'date' :
|
|
70
|
+
token.type === TokenType.TIME ? 'time' : 'datetime',
|
|
71
|
+
token.position
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse special variables
|
|
80
|
+
*/
|
|
81
|
+
static parseVariable(navigator: TokenNavigator): ASTNode | null {
|
|
82
|
+
if (navigator.match(TokenType.THIS)) {
|
|
83
|
+
return ASTFactory.createVariable('$this', true, navigator.previous().position);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (navigator.match(TokenType.INDEX)) {
|
|
87
|
+
return ASTFactory.createVariable('$index', true, navigator.previous().position);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (navigator.match(TokenType.TOTAL)) {
|
|
91
|
+
return ASTFactory.createVariable('$total', true, navigator.previous().position);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (navigator.match(TokenType.ENV_VAR)) {
|
|
95
|
+
const token = navigator.previous();
|
|
96
|
+
return ASTFactory.createVariable(token.value, false, token.position);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Variables starting with $ or % are handled by ENV_VAR token type
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Token } from '../lexer/token';
|
|
2
|
+
import type { Position } from './ast';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown during parsing
|
|
6
|
+
*/
|
|
7
|
+
export class ParseError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
public position: Position,
|
|
11
|
+
public token: Token
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ParseError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Token } from '../lexer/token';
|
|
2
|
+
import type { Position } from './ast';
|
|
3
|
+
import { ParseError } from './parser';
|
|
4
|
+
import { ErrorCode } from '../api/errors';
|
|
5
|
+
import type { DiagnosticCollector } from './diagnostics';
|
|
6
|
+
import type { SourceMapper } from './source-mapper';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Factory for creating parser errors with consistent formatting
|
|
10
|
+
*/
|
|
11
|
+
export class ParserErrorFactory {
|
|
12
|
+
constructor(
|
|
13
|
+
private throwOnError: boolean,
|
|
14
|
+
private diagnostics?: DiagnosticCollector,
|
|
15
|
+
private sourceMapper?: SourceMapper
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a parse error with proper formatting
|
|
20
|
+
*/
|
|
21
|
+
createError(message: string, token: Token, code: ErrorCode = ErrorCode.PARSE_ERROR): ParseError {
|
|
22
|
+
if (this.diagnostics && this.sourceMapper && !this.throwOnError) {
|
|
23
|
+
// In diagnostic mode, add to diagnostics and return error
|
|
24
|
+
const range = this.sourceMapper.tokenToRange(token);
|
|
25
|
+
this.diagnostics.addError(range, message, code);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pos = token.position;
|
|
29
|
+
const fullMessage = `${message} at line ${pos.line}, column ${pos.column}`;
|
|
30
|
+
return new ParseError(fullMessage, pos, token);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create error for unexpected token
|
|
35
|
+
*/
|
|
36
|
+
unexpectedToken(token: Token, expected?: string): ParseError {
|
|
37
|
+
const message = expected
|
|
38
|
+
? `Expected ${expected}, found '${token.value}'`
|
|
39
|
+
: `Unexpected token '${token.value}'`;
|
|
40
|
+
return this.createError(message, token, ErrorCode.UNEXPECTED_TOKEN);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create error for missing token
|
|
45
|
+
*/
|
|
46
|
+
missingToken(expected: string, token: Token, code: ErrorCode = ErrorCode.UNEXPECTED_TOKEN): ParseError {
|
|
47
|
+
return this.createError(`Expected ${expected}`, token, code);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create error for unclosed delimiter
|
|
52
|
+
*/
|
|
53
|
+
unclosedDelimiter(openToken: Token, expected: string): ParseError {
|
|
54
|
+
const code = expected === ')' ? ErrorCode.UNCLOSED_PARENTHESIS :
|
|
55
|
+
expected === ']' ? ErrorCode.UNCLOSED_BRACKET :
|
|
56
|
+
expected === '}' ? ErrorCode.UNCLOSED_BRACE :
|
|
57
|
+
ErrorCode.SYNTAX_ERROR;
|
|
58
|
+
|
|
59
|
+
return this.createError(
|
|
60
|
+
`Unclosed ${this.getDelimiterName(expected)} - missing '${expected}'`,
|
|
61
|
+
openToken,
|
|
62
|
+
code
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create error for invalid operator
|
|
68
|
+
*/
|
|
69
|
+
invalidOperator(token: Token): ParseError {
|
|
70
|
+
return this.createError(`Unknown operator: ${token.value}`, token, ErrorCode.INVALID_OPERATOR);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create error for missing arguments
|
|
75
|
+
*/
|
|
76
|
+
missingArguments(functionName: string, token: Token): ParseError {
|
|
77
|
+
return this.createError(
|
|
78
|
+
`Function '${functionName}' requires arguments`,
|
|
79
|
+
token,
|
|
80
|
+
ErrorCode.MISSING_ARGUMENTS
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create error for expected identifier
|
|
86
|
+
*/
|
|
87
|
+
expectedIdentifier(token: Token, context?: string): ParseError {
|
|
88
|
+
const message = context
|
|
89
|
+
? `Expected identifier ${context}`
|
|
90
|
+
: `Expected identifier`;
|
|
91
|
+
return this.createError(message, token, ErrorCode.EXPECTED_IDENTIFIER);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create error for expected expression
|
|
96
|
+
*/
|
|
97
|
+
expectedExpression(token: Token, context?: string): ParseError {
|
|
98
|
+
const message = context
|
|
99
|
+
? `Expected expression ${context}`
|
|
100
|
+
: `Expected expression`;
|
|
101
|
+
return this.createError(message, token, ErrorCode.EXPECTED_EXPRESSION);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create error for double dot operator
|
|
106
|
+
*/
|
|
107
|
+
doubleDotOperator(firstDot: Token, secondDot: Token): ParseError {
|
|
108
|
+
const message = `Invalid '..' operator - use a single '.' for navigation`;
|
|
109
|
+
if (this.diagnostics && this.sourceMapper) {
|
|
110
|
+
const range = this.sourceMapper.mergeRanges(
|
|
111
|
+
this.sourceMapper.tokenToRange(firstDot),
|
|
112
|
+
this.sourceMapper.tokenToRange(secondDot)
|
|
113
|
+
);
|
|
114
|
+
this.diagnostics.addError(range, message, ErrorCode.INVALID_OPERATOR);
|
|
115
|
+
}
|
|
116
|
+
return this.createError(message, firstDot, ErrorCode.INVALID_OPERATOR);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create error for invalid escape sequence
|
|
121
|
+
*/
|
|
122
|
+
invalidEscape(token: Token, sequence: string): ParseError {
|
|
123
|
+
return this.createError(
|
|
124
|
+
`Invalid escape sequence: ${sequence}`,
|
|
125
|
+
token,
|
|
126
|
+
ErrorCode.INVALID_ESCAPE
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get human-readable name for delimiter
|
|
132
|
+
*/
|
|
133
|
+
private getDelimiterName(delimiter: string): string {
|
|
134
|
+
switch (delimiter) {
|
|
135
|
+
case ')': return 'parenthesis';
|
|
136
|
+
case ']': return 'bracket';
|
|
137
|
+
case '}': return 'brace';
|
|
138
|
+
default: return 'delimiter';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|