@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,1272 @@
|
|
|
1
|
+
import type { Token } from '../lexer/token';
|
|
2
|
+
import { TokenType } from '../lexer/token';
|
|
3
|
+
import { lex } from '../lexer/lexer';
|
|
4
|
+
import type { Position } from './ast';
|
|
5
|
+
import {
|
|
6
|
+
type ASTNode,
|
|
7
|
+
type BinaryNode,
|
|
8
|
+
type UnaryNode,
|
|
9
|
+
type LiteralNode,
|
|
10
|
+
type IdentifierNode,
|
|
11
|
+
type TypeOrIdentifierNode,
|
|
12
|
+
type FunctionNode,
|
|
13
|
+
type VariableNode,
|
|
14
|
+
type IndexNode,
|
|
15
|
+
type UnionNode,
|
|
16
|
+
type MembershipTestNode,
|
|
17
|
+
type TypeCastNode,
|
|
18
|
+
type CollectionNode,
|
|
19
|
+
type TypeReferenceNode,
|
|
20
|
+
type ErrorNode,
|
|
21
|
+
type IncompleteNode,
|
|
22
|
+
NodeType
|
|
23
|
+
} from './ast';
|
|
24
|
+
import { Registry } from '../registry';
|
|
25
|
+
import {
|
|
26
|
+
type ParserOptions,
|
|
27
|
+
type ParseResult,
|
|
28
|
+
type TextRange
|
|
29
|
+
} from './types';
|
|
30
|
+
import { DiagnosticCollector } from './diagnostics';
|
|
31
|
+
import { SourceMapper } from './source-mapper';
|
|
32
|
+
import { ContextualErrorReporter, ParseContext } from './error-reporter';
|
|
33
|
+
import { FHIRPathDiagnostics } from './diagnostic-messages';
|
|
34
|
+
import { ErrorCode } from '../api/errors';
|
|
35
|
+
import { TokenNavigator } from './token-navigator';
|
|
36
|
+
import { ASTFactory } from './ast-factory';
|
|
37
|
+
import { ParserErrorFactory } from './parser-error-factory';
|
|
38
|
+
import { ParserState } from './parser-state';
|
|
39
|
+
import { PrecedenceManager } from './precedence-manager';
|
|
40
|
+
import { ParseError } from './parse-error';
|
|
41
|
+
import { LiteralParser } from './literal-parser';
|
|
42
|
+
import { CollectionParser } from './collection-parser';
|
|
43
|
+
import { SpecialConstructs } from './special-constructs';
|
|
44
|
+
|
|
45
|
+
// Re-export ParseError for backward compatibility
|
|
46
|
+
export { ParseError } from './parse-error';
|
|
47
|
+
|
|
48
|
+
// Precedence levels for reference (from spec)
|
|
49
|
+
// Parser uses standard precedence (higher number = higher precedence)
|
|
50
|
+
// IMPLIES: 1, // implies - lowest precedence
|
|
51
|
+
// OR: 2, // or, xor
|
|
52
|
+
// AND: 3, // and
|
|
53
|
+
// MEMBERSHIP: 4, // in, contains
|
|
54
|
+
// EQUALITY: 5, // =, ~, !=, !~
|
|
55
|
+
// RELATIONAL: 6, // <, >, <=, >=
|
|
56
|
+
// UNION: 7, // |
|
|
57
|
+
// TYPE: 8, // is, as
|
|
58
|
+
// ADDITIVE: 9, // +, -, &
|
|
59
|
+
// MULTIPLICATIVE: 10, // *, /, div, mod
|
|
60
|
+
// UNARY: 11, // unary +, -, not
|
|
61
|
+
// POSTFIX: 12, // [] indexing
|
|
62
|
+
// INVOCATION: 13, // . (dot), function calls - highest precedence
|
|
63
|
+
|
|
64
|
+
export class FHIRPathParser {
|
|
65
|
+
private navigator: TokenNavigator;
|
|
66
|
+
private throwOnError: boolean;
|
|
67
|
+
private trackRanges: boolean;
|
|
68
|
+
private errorRecovery: boolean;
|
|
69
|
+
private diagnostics?: DiagnosticCollector;
|
|
70
|
+
private sourceMapper?: SourceMapper;
|
|
71
|
+
private errorReporter?: ContextualErrorReporter;
|
|
72
|
+
private errorFactory: ParserErrorFactory;
|
|
73
|
+
private state: ParserState;
|
|
74
|
+
private input: string;
|
|
75
|
+
private tokens: Token[];
|
|
76
|
+
|
|
77
|
+
constructor(input: string | Token[], options: ParserOptions = {}) {
|
|
78
|
+
this.throwOnError = options.throwOnError ?? false;
|
|
79
|
+
this.trackRanges = options.trackRanges ?? false;
|
|
80
|
+
this.errorRecovery = options.errorRecovery ?? false;
|
|
81
|
+
|
|
82
|
+
if (typeof input === 'string') {
|
|
83
|
+
this.input = input;
|
|
84
|
+
try {
|
|
85
|
+
this.tokens = lex(input);
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
// If throwOnError is true, re-throw lexer errors
|
|
88
|
+
if (this.throwOnError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
// Otherwise, we'll handle lexer errors in parse()
|
|
92
|
+
this.tokens = [];
|
|
93
|
+
// Store the lexer error for later processing
|
|
94
|
+
(this as any).lexerError = error;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
this.tokens = input;
|
|
98
|
+
// For token array input, we'll construct a minimal input string
|
|
99
|
+
this.input = input.map(t => t.value).join(' ');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.navigator = new TokenNavigator(this.tokens);
|
|
103
|
+
this.state = new ParserState();
|
|
104
|
+
this.initializeForMode(this.input, options);
|
|
105
|
+
// Create error factory after diagnostics and sourceMapper are initialized
|
|
106
|
+
this.errorFactory = new ParserErrorFactory(this.throwOnError, this.diagnostics, this.sourceMapper);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private initializeForMode(input: string, options: ParserOptions): void {
|
|
110
|
+
// Always create diagnostic collector unless throwOnError is true
|
|
111
|
+
if (!this.throwOnError) {
|
|
112
|
+
this.diagnostics = new DiagnosticCollector(options.maxErrors);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Only create source mapper if needed
|
|
116
|
+
if (this.trackRanges || !this.throwOnError) {
|
|
117
|
+
this.sourceMapper = new SourceMapper(input);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Only create error reporter if error recovery is enabled
|
|
121
|
+
if (this.errorRecovery && this.diagnostics && this.sourceMapper) {
|
|
122
|
+
this.errorReporter = new ContextualErrorReporter(this.sourceMapper, this.diagnostics);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Main entry point
|
|
127
|
+
parse(): ParseResult {
|
|
128
|
+
if (this.errorRecovery) {
|
|
129
|
+
return this.parseWithRecovery();
|
|
130
|
+
} else {
|
|
131
|
+
return this.parseStandard();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
private parseStandard(): ParseResult {
|
|
137
|
+
let ast: ASTNode;
|
|
138
|
+
|
|
139
|
+
// Check for lexer errors first
|
|
140
|
+
if ((this as any).lexerError) {
|
|
141
|
+
const error = (this as any).lexerError;
|
|
142
|
+
|
|
143
|
+
// If throwOnError is true, throw immediately
|
|
144
|
+
if (this.throwOnError) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Map lexer error positions to our range format
|
|
149
|
+
const range: TextRange = {
|
|
150
|
+
start: {
|
|
151
|
+
line: error.position.line - 1, // Lexer uses 1-based lines, we use 0-based
|
|
152
|
+
character: error.position.column - 1, // Lexer uses 1-based columns, we use 0-based
|
|
153
|
+
offset: error.position.offset
|
|
154
|
+
},
|
|
155
|
+
end: {
|
|
156
|
+
line: error.position.line - 1,
|
|
157
|
+
character: error.position.column, // One character after the error
|
|
158
|
+
offset: error.position.offset + 1
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Map specific lexer errors to appropriate error codes
|
|
163
|
+
let errorCode = ErrorCode.SYNTAX_ERROR;
|
|
164
|
+
if (error.message.includes('Invalid escape sequence')) {
|
|
165
|
+
errorCode = ErrorCode.INVALID_ESCAPE;
|
|
166
|
+
} else if (error.message.includes('Unterminated string')) {
|
|
167
|
+
errorCode = ErrorCode.UNTERMINATED_STRING;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.diagnostics!.addError(range, error.message, errorCode);
|
|
171
|
+
|
|
172
|
+
// Return minimal AST with error
|
|
173
|
+
return {
|
|
174
|
+
ast: {
|
|
175
|
+
type: NodeType.Literal,
|
|
176
|
+
value: null,
|
|
177
|
+
valueType: 'null',
|
|
178
|
+
position: { line: 1, column: 1, offset: 0 }
|
|
179
|
+
} as LiteralNode,
|
|
180
|
+
diagnostics: this.diagnostics!.getDiagnostics(),
|
|
181
|
+
hasErrors: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
ast = this.expression();
|
|
187
|
+
|
|
188
|
+
if (!this.isAtEnd()) {
|
|
189
|
+
if (this.throwOnError) {
|
|
190
|
+
throw this.error("Unexpected token after expression", ErrorCode.UNEXPECTED_TOKEN);
|
|
191
|
+
}
|
|
192
|
+
// Otherwise, report as diagnostic
|
|
193
|
+
const unexpectedToken = this.peek();
|
|
194
|
+
const range = this.sourceMapper!.tokenToRange(unexpectedToken);
|
|
195
|
+
this.diagnostics!.addError(
|
|
196
|
+
range,
|
|
197
|
+
`Unexpected token '${unexpectedToken.value}' after expression`,
|
|
198
|
+
ErrorCode.UNEXPECTED_TOKEN
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (this.throwOnError) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
// Otherwise, try to recover and report diagnostic
|
|
206
|
+
if (error instanceof ParseError) {
|
|
207
|
+
// Don't add the error again - it was already added in the error() method
|
|
208
|
+
// Just create a minimal error AST node
|
|
209
|
+
ast = {
|
|
210
|
+
type: NodeType.Literal,
|
|
211
|
+
value: null,
|
|
212
|
+
valueType: 'null',
|
|
213
|
+
position: error.position
|
|
214
|
+
} as LiteralNode;
|
|
215
|
+
} else {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result: ParseResult = {
|
|
221
|
+
ast,
|
|
222
|
+
diagnostics: this.diagnostics ? this.diagnostics.getDiagnostics() : [],
|
|
223
|
+
hasErrors: this.diagnostics ? this.diagnostics.hasErrors() : false
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Add ranges if tracking is enabled
|
|
227
|
+
if (this.trackRanges && this.sourceMapper) {
|
|
228
|
+
result.ranges = this.collectNodeRanges(ast);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private parseWithRecovery(): ParseResult {
|
|
235
|
+
let ast: ASTNode;
|
|
236
|
+
const ranges = this.trackRanges ? new Map<ASTNode, TextRange>() : undefined;
|
|
237
|
+
|
|
238
|
+
// Check for lexer errors first
|
|
239
|
+
if ((this as any).lexerError) {
|
|
240
|
+
const error = (this as any).lexerError;
|
|
241
|
+
// Map lexer error positions to our range format
|
|
242
|
+
const range: TextRange = {
|
|
243
|
+
start: {
|
|
244
|
+
line: error.position.line - 1, // Lexer uses 1-based lines, we use 0-based
|
|
245
|
+
character: error.position.column - 1, // Lexer uses 1-based columns, we use 0-based
|
|
246
|
+
offset: error.position.offset
|
|
247
|
+
},
|
|
248
|
+
end: {
|
|
249
|
+
line: error.position.line - 1,
|
|
250
|
+
character: error.position.column, // One character after the error
|
|
251
|
+
offset: error.position.offset + 1
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Map specific lexer errors to appropriate error codes
|
|
256
|
+
let errorCode = ErrorCode.SYNTAX_ERROR;
|
|
257
|
+
if (error.message.includes('Invalid escape sequence')) {
|
|
258
|
+
errorCode = ErrorCode.INVALID_ESCAPE;
|
|
259
|
+
} else if (error.message.includes('Unterminated string')) {
|
|
260
|
+
errorCode = ErrorCode.UNTERMINATED_STRING;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.diagnostics!.addError(range, error.message, errorCode);
|
|
264
|
+
|
|
265
|
+
// Create error node
|
|
266
|
+
ast = {
|
|
267
|
+
type: NodeType.Error,
|
|
268
|
+
position: { line: 1, column: 1, offset: 0 },
|
|
269
|
+
expectedTokens: [],
|
|
270
|
+
diagnostic: {
|
|
271
|
+
severity: 1,
|
|
272
|
+
range,
|
|
273
|
+
message: error.message,
|
|
274
|
+
code: errorCode,
|
|
275
|
+
source: 'fhirpath-parser'
|
|
276
|
+
}
|
|
277
|
+
} as ErrorNode;
|
|
278
|
+
|
|
279
|
+
const result: ParseResult = {
|
|
280
|
+
ast,
|
|
281
|
+
diagnostics: this.diagnostics!.getDiagnostics(),
|
|
282
|
+
hasErrors: true,
|
|
283
|
+
isPartial: true
|
|
284
|
+
};
|
|
285
|
+
if (this.trackRanges && ranges) {
|
|
286
|
+
result.ranges = ranges;
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
ast = this.expressionWithRecovery();
|
|
293
|
+
|
|
294
|
+
if (!this.isAtEnd()) {
|
|
295
|
+
// In diagnostic mode, try to consume remaining tokens
|
|
296
|
+
const unexpectedToken = this.peek();
|
|
297
|
+
this.errorReporter!.reportExpectedToken(
|
|
298
|
+
[TokenType.EOF],
|
|
299
|
+
unexpectedToken,
|
|
300
|
+
ParseContext.Expression
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Try to recover by consuming remaining tokens
|
|
304
|
+
while (!this.isAtEnd()) {
|
|
305
|
+
this.advance();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// In diagnostic mode, create error node and mark as partial
|
|
310
|
+
if (error instanceof ParseError) {
|
|
311
|
+
ast = this.createErrorNode(error.token, error.message);
|
|
312
|
+
this.state.markPartial();
|
|
313
|
+
} else {
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const result: ParseResult = {
|
|
319
|
+
ast,
|
|
320
|
+
diagnostics: this.diagnostics!.getDiagnostics(),
|
|
321
|
+
hasErrors: this.diagnostics!.hasErrors()
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (this.errorRecovery) {
|
|
325
|
+
result.isPartial = this.state.getIsPartial();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (this.trackRanges && this.sourceMapper) {
|
|
329
|
+
result.ranges = this.collectNodeRanges(ast);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Pratt parser for expressions
|
|
336
|
+
private expression(minPrecedence: number = 0): ASTNode {
|
|
337
|
+
let left = this.primary();
|
|
338
|
+
|
|
339
|
+
while (!this.isAtEnd()) {
|
|
340
|
+
// Handle postfix operators first
|
|
341
|
+
if (this.check(TokenType.LBRACKET) && minPrecedence <= 12) { // POSTFIX precedence
|
|
342
|
+
left = this.parseIndex(left);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Handle function calls that come from primary() or after dots
|
|
347
|
+
// This allows for chained method calls like exists().not()
|
|
348
|
+
if (this.check(TokenType.DOT) && minPrecedence <= 13) { // INVOCATION precedence
|
|
349
|
+
const dotToken = this.peek();
|
|
350
|
+
const precedence = PrecedenceManager.getPrecedence(dotToken);
|
|
351
|
+
if (precedence < minPrecedence) break;
|
|
352
|
+
|
|
353
|
+
left = this.parseBinary(left, dotToken, precedence);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const token = this.peek();
|
|
358
|
+
const precedence = PrecedenceManager.getPrecedence(token);
|
|
359
|
+
|
|
360
|
+
if (precedence === 0 || precedence < minPrecedence) break;
|
|
361
|
+
|
|
362
|
+
left = this.parseBinary(left, token, precedence);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return left;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Parse primary expressions (recursive descent)
|
|
369
|
+
private primary(): ASTNode {
|
|
370
|
+
// Try parsing literals
|
|
371
|
+
const literal = LiteralParser.parseLiteral(this.navigator);
|
|
372
|
+
if (literal) return literal;
|
|
373
|
+
|
|
374
|
+
// Try parsing variables
|
|
375
|
+
const variable = LiteralParser.parseVariable(this.navigator);
|
|
376
|
+
if (variable) return variable;
|
|
377
|
+
|
|
378
|
+
// Handle grouping
|
|
379
|
+
if (this.match(TokenType.LPAREN)) {
|
|
380
|
+
const expr = this.expression();
|
|
381
|
+
this.consume(TokenType.RPAREN, "Expected ')' after expression", ErrorCode.UNCLOSED_PARENTHESIS);
|
|
382
|
+
|
|
383
|
+
// Check for method calls after parentheses (e.g., (expr).method())
|
|
384
|
+
let result = expr;
|
|
385
|
+
while (this.check(TokenType.DOT)) {
|
|
386
|
+
const dotToken = this.advance();
|
|
387
|
+
|
|
388
|
+
// After a dot, handle keywords that can be method names
|
|
389
|
+
let right: ASTNode;
|
|
390
|
+
const next = this.peek();
|
|
391
|
+
if (this.isOperatorKeyword(next.type)) {
|
|
392
|
+
// Treat keyword as identifier
|
|
393
|
+
this.advance();
|
|
394
|
+
right = {
|
|
395
|
+
type: NodeType.Identifier,
|
|
396
|
+
name: next.value,
|
|
397
|
+
position: next.position
|
|
398
|
+
} as IdentifierNode;
|
|
399
|
+
} else {
|
|
400
|
+
right = this.primary();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// If the right side is a function call, handle it
|
|
404
|
+
if (this.check(TokenType.LPAREN)) {
|
|
405
|
+
const methodNode: BinaryNode = {
|
|
406
|
+
type: NodeType.Binary,
|
|
407
|
+
operator: TokenType.DOT,
|
|
408
|
+
operation: Registry.getByToken(TokenType.DOT),
|
|
409
|
+
left: result,
|
|
410
|
+
right: right,
|
|
411
|
+
position: dotToken.position
|
|
412
|
+
};
|
|
413
|
+
result = this.functionCall(methodNode);
|
|
414
|
+
} else {
|
|
415
|
+
// Regular property access
|
|
416
|
+
result = {
|
|
417
|
+
type: NodeType.Binary,
|
|
418
|
+
operator: TokenType.DOT,
|
|
419
|
+
operation: Registry.getByToken(TokenType.DOT),
|
|
420
|
+
left: result,
|
|
421
|
+
right: right,
|
|
422
|
+
position: dotToken.position
|
|
423
|
+
} as BinaryNode;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Handle identifiers (which might be followed by function calls)
|
|
431
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
432
|
+
return this.identifierOrFunctionCall();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Handle delimited identifiers
|
|
436
|
+
if (this.match(TokenType.DELIMITED_IDENTIFIER)) {
|
|
437
|
+
return this.identifierOrFunctionCall();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Handle unary operators
|
|
441
|
+
if (this.match(TokenType.PLUS, TokenType.MINUS, TokenType.NOT)) {
|
|
442
|
+
const op = this.previous();
|
|
443
|
+
const operation = op.operation || Registry.getByToken(op.type);
|
|
444
|
+
const right = this.expression(3); // UNARY precedence
|
|
445
|
+
return {
|
|
446
|
+
type: NodeType.Unary,
|
|
447
|
+
operator: op.type,
|
|
448
|
+
operation: operation,
|
|
449
|
+
operand: right,
|
|
450
|
+
position: op.position
|
|
451
|
+
} as UnaryNode;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Handle empty collection {} or collection literals {expr1, expr2, ...}
|
|
455
|
+
if (this.match(TokenType.LBRACE)) {
|
|
456
|
+
const lbrace = this.previous();
|
|
457
|
+
const startPos = lbrace.position;
|
|
458
|
+
const elements: ASTNode[] = [];
|
|
459
|
+
|
|
460
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
461
|
+
const previousContext = this.state.getContext();
|
|
462
|
+
this.state.setContext('CollectionLiteral');
|
|
463
|
+
|
|
464
|
+
do {
|
|
465
|
+
// Check for trailing comma before trying to parse expression
|
|
466
|
+
if (this.check(TokenType.RBRACE)) {
|
|
467
|
+
// We have a trailing comma
|
|
468
|
+
if (this.diagnostics) {
|
|
469
|
+
const commaToken = this.previous(); // The comma we just consumed
|
|
470
|
+
const diagnostic = FHIRPathDiagnostics.trailingComma(commaToken, this.sourceMapper!);
|
|
471
|
+
this.diagnostics!.addError(diagnostic.range, diagnostic.message, diagnostic.code);
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
elements.push(this.errorRecovery ? this.expressionWithRecovery() : this.expression());
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
480
|
+
elements.push(this.createErrorNode(error.token, error.message));
|
|
481
|
+
// Skip to next comma or closing brace
|
|
482
|
+
while (!this.isAtEnd() && !this.check(TokenType.COMMA) && !this.check(TokenType.RBRACE)) {
|
|
483
|
+
this.advance();
|
|
484
|
+
}
|
|
485
|
+
if (this.check(TokenType.COMMA)) {
|
|
486
|
+
continue;
|
|
487
|
+
} else {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} while (this.match(TokenType.COMMA));
|
|
495
|
+
|
|
496
|
+
this.state.setContext(previousContext);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!this.match(TokenType.RBRACE)) {
|
|
500
|
+
if (this.diagnostics) {
|
|
501
|
+
this.errorReporter?.reportUnclosedDelimiter(lbrace, '}') ||
|
|
502
|
+
this.diagnostics!.addError(
|
|
503
|
+
this.sourceMapper!.tokenToRange(lbrace),
|
|
504
|
+
"Expected '}' to close collection literal",
|
|
505
|
+
ErrorCode.UNCLOSED_BRACE
|
|
506
|
+
);
|
|
507
|
+
this.state.markPartial();
|
|
508
|
+
} else {
|
|
509
|
+
throw this.error("Expected '}' after collection elements", ErrorCode.UNCLOSED_BRACE);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Restore context if we set it earlier
|
|
514
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
515
|
+
this.state.setContext('Expression');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
type: NodeType.Collection,
|
|
520
|
+
elements: elements,
|
|
521
|
+
position: startPos
|
|
522
|
+
} as CollectionNode;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Handle operator keywords as identifiers/functions at expression start
|
|
526
|
+
if (this.isOperatorKeyword(this.peek().type)) {
|
|
527
|
+
const token = this.advance();
|
|
528
|
+
const identifier: IdentifierNode = {
|
|
529
|
+
type: NodeType.Identifier,
|
|
530
|
+
name: token.value,
|
|
531
|
+
position: token.position
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Check for function call
|
|
535
|
+
if (this.check(TokenType.LPAREN)) {
|
|
536
|
+
return this.functionCall(identifier);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return identifier;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const token = this.peek();
|
|
543
|
+
let message = "Expected expression";
|
|
544
|
+
|
|
545
|
+
// Add context-specific information
|
|
546
|
+
const context = this.state.getContext();
|
|
547
|
+
if (context === 'CollectionLiteral') {
|
|
548
|
+
message = "Expected expression in collection";
|
|
549
|
+
} else if (context === 'FunctionCall') {
|
|
550
|
+
message = "Expected expression in function call";
|
|
551
|
+
} else if (context === 'IndexExpression') {
|
|
552
|
+
message = "Expected expression in index";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (token.type !== TokenType.EOF) {
|
|
556
|
+
message += `, found '${token.value}'`;
|
|
557
|
+
}
|
|
558
|
+
throw this.error(message, ErrorCode.EXPECTED_EXPRESSION);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Parse binary operators with precedence
|
|
562
|
+
private parseBinary(left: ASTNode, op: Token, precedence: number): ASTNode {
|
|
563
|
+
const operation = op.operation || Registry.getByToken(op.type);
|
|
564
|
+
if (!operation && op.type !== TokenType.DOT && op.type !== TokenType.IS && op.type !== TokenType.AS) {
|
|
565
|
+
throw this.error(`Unknown operator: ${op.value}`, ErrorCode.INVALID_OPERATOR);
|
|
566
|
+
}
|
|
567
|
+
// Special handling for type operators
|
|
568
|
+
if (op.type === TokenType.IS || op.type === TokenType.AS) {
|
|
569
|
+
this.advance(); // consume operator
|
|
570
|
+
|
|
571
|
+
// Type name can be either a simple identifier or in parentheses (for is() function syntax)
|
|
572
|
+
let typeName: string;
|
|
573
|
+
if (this.check(TokenType.LPAREN)) {
|
|
574
|
+
// Handle is(TypeName) syntax
|
|
575
|
+
this.advance(); // consume (
|
|
576
|
+
typeName = this.consume(TokenType.IDENTIFIER, "Expected type name", ErrorCode.EXPECTED_IDENTIFIER).value;
|
|
577
|
+
this.consume(TokenType.RPAREN, "Expected ')' after type name", ErrorCode.UNCLOSED_PARENTHESIS);
|
|
578
|
+
} else {
|
|
579
|
+
// Regular is TypeName syntax
|
|
580
|
+
const typeToken = this.peek();
|
|
581
|
+
if (!this.match(TokenType.IDENTIFIER)) {
|
|
582
|
+
if (this.errorRecovery) {
|
|
583
|
+
const context = op.type === TokenType.AS ? ParseContext.TypeCast : ParseContext.MembershipTest;
|
|
584
|
+
this.errorReporter!.reportExpectedToken(
|
|
585
|
+
[TokenType.IDENTIFIER],
|
|
586
|
+
typeToken,
|
|
587
|
+
context
|
|
588
|
+
);
|
|
589
|
+
// Create incomplete node
|
|
590
|
+
return this.createIncompleteNode(left, ['type name']);
|
|
591
|
+
} else if (!this.errorRecovery && this.diagnostics) {
|
|
592
|
+
// In standard mode, add error once and throw
|
|
593
|
+
const range = this.sourceMapper!.tokenToRange(typeToken);
|
|
594
|
+
this.diagnostics!.addError(
|
|
595
|
+
range,
|
|
596
|
+
`Expected type name after '${op.value}'`,
|
|
597
|
+
ErrorCode.EXPECTED_IDENTIFIER
|
|
598
|
+
);
|
|
599
|
+
// Throw without adding duplicate diagnostic
|
|
600
|
+
const pos = typeToken.position;
|
|
601
|
+
const fullMessage = `Expected type name at line ${pos.line}, column ${pos.column}`;
|
|
602
|
+
throw new ParseError(fullMessage, pos, typeToken);
|
|
603
|
+
}
|
|
604
|
+
throw this.error("Expected type name", ErrorCode.EXPECTED_IDENTIFIER);
|
|
605
|
+
}
|
|
606
|
+
typeName = this.previous().value;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
type: op.type === TokenType.IS ? NodeType.MembershipTest : NodeType.TypeCast,
|
|
611
|
+
expression: left,
|
|
612
|
+
targetType: typeName,
|
|
613
|
+
position: op.position
|
|
614
|
+
} as MembershipTestNode | TypeCastNode;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.advance(); // consume operator
|
|
618
|
+
|
|
619
|
+
// Check for common mistake: == instead of =
|
|
620
|
+
if (op.type === TokenType.EQ && this.check(TokenType.EQ)) {
|
|
621
|
+
const secondEq = this.peek();
|
|
622
|
+
if (this.diagnostics) {
|
|
623
|
+
const range = this.sourceMapper!.mergeRanges(
|
|
624
|
+
this.sourceMapper!.tokenToRange(op),
|
|
625
|
+
this.sourceMapper!.tokenToRange(secondEq)
|
|
626
|
+
);
|
|
627
|
+
this.diagnostics!.addError(
|
|
628
|
+
range,
|
|
629
|
+
"'==' is not valid in FHIRPath, use '=' for equality",
|
|
630
|
+
ErrorCode.INVALID_OPERATOR
|
|
631
|
+
);
|
|
632
|
+
// Skip the extra = to avoid cascading errors
|
|
633
|
+
this.advance();
|
|
634
|
+
} else if (this.throwOnError) {
|
|
635
|
+
throw this.error("'==' is not valid in FHIRPath, use '=' for equality", ErrorCode.INVALID_OPERATOR);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Special handling for union operator - can chain multiple
|
|
640
|
+
if (op.type === TokenType.PIPE) {
|
|
641
|
+
const right = this.expression(precedence + 1);
|
|
642
|
+
|
|
643
|
+
// If left is already a union, add to it
|
|
644
|
+
if (left.type === NodeType.Union) {
|
|
645
|
+
(left as UnionNode).operands.push(right);
|
|
646
|
+
return left;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Create new union node
|
|
650
|
+
return {
|
|
651
|
+
type: NodeType.Union,
|
|
652
|
+
operands: [left, right],
|
|
653
|
+
position: op.position
|
|
654
|
+
} as UnionNode;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Special handling for dot operator (left-associative, pipelines data)
|
|
658
|
+
if (op.type === TokenType.DOT) {
|
|
659
|
+
// Check for double dot error
|
|
660
|
+
if (this.check(TokenType.DOT)) {
|
|
661
|
+
const secondDot = this.peek();
|
|
662
|
+
if (this.diagnostics) {
|
|
663
|
+
const diagnostic = FHIRPathDiagnostics.doubleDotOperator(op, secondDot, this.sourceMapper!);
|
|
664
|
+
this.diagnostics!.addError(diagnostic.range, diagnostic.message, diagnostic.code);
|
|
665
|
+
|
|
666
|
+
// Skip the extra dot to avoid cascading errors
|
|
667
|
+
this.advance();
|
|
668
|
+
|
|
669
|
+
if (this.errorRecovery) {
|
|
670
|
+
this.state.markPartial();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (this.throwOnError) {
|
|
675
|
+
throw this.error("Invalid '..' operator - use single '.' for navigation", ErrorCode.INVALID_OPERATOR);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// After a dot, we need to handle keywords that can be method names
|
|
680
|
+
let right: ASTNode;
|
|
681
|
+
|
|
682
|
+
// Check if next token is a keyword that can be used as a method name
|
|
683
|
+
const next = this.peek();
|
|
684
|
+
if (this.isOperatorKeyword(next.type)) {
|
|
685
|
+
// Treat keyword as identifier
|
|
686
|
+
this.advance();
|
|
687
|
+
right = {
|
|
688
|
+
type: NodeType.Identifier,
|
|
689
|
+
name: next.value,
|
|
690
|
+
position: next.position
|
|
691
|
+
} as IdentifierNode;
|
|
692
|
+
} else {
|
|
693
|
+
// Check for invalid syntax like .[ or .123
|
|
694
|
+
if (this.check(TokenType.LBRACKET) || this.check(TokenType.NUMBER)) {
|
|
695
|
+
if (this.diagnostics) {
|
|
696
|
+
this.errorReporter?.reportExpectedToken(
|
|
697
|
+
[TokenType.IDENTIFIER],
|
|
698
|
+
this.peek(),
|
|
699
|
+
ParseContext.Expression
|
|
700
|
+
) || this.diagnostics!.addError(
|
|
701
|
+
this.sourceMapper!.tokenToRange(this.peek()),
|
|
702
|
+
"Expected property name after '.'",
|
|
703
|
+
ErrorCode.EXPECTED_IDENTIFIER
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (this.errorRecovery) {
|
|
707
|
+
// Create error node and continue to parse the index
|
|
708
|
+
right = this.createErrorNode(this.peek(), "Expected identifier");
|
|
709
|
+
return {
|
|
710
|
+
type: NodeType.Binary,
|
|
711
|
+
operator: TokenType.DOT,
|
|
712
|
+
operation: operation,
|
|
713
|
+
left: left,
|
|
714
|
+
right: right,
|
|
715
|
+
position: op.position
|
|
716
|
+
} as BinaryNode;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
throw this.error("Expected property name after '.'", ErrorCode.EXPECTED_IDENTIFIER);;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
right = this.primary();
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
726
|
+
// Report missing identifier after dot
|
|
727
|
+
this.errorReporter!.reportMissingIdentifier(this.peek(), "after '.'");
|
|
728
|
+
return this.createIncompleteNode(left, ['property']);
|
|
729
|
+
}
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check for function call after dot
|
|
735
|
+
if (this.peek().type === TokenType.LPAREN) {
|
|
736
|
+
const dotNode: BinaryNode = {
|
|
737
|
+
type: NodeType.Binary,
|
|
738
|
+
operator: TokenType.DOT,
|
|
739
|
+
operation: operation,
|
|
740
|
+
left: left,
|
|
741
|
+
right: right,
|
|
742
|
+
position: op.position
|
|
743
|
+
};
|
|
744
|
+
return this.functionCall(dotNode);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
type: NodeType.Binary,
|
|
749
|
+
operator: TokenType.DOT,
|
|
750
|
+
operation: operation,
|
|
751
|
+
left: left,
|
|
752
|
+
right: right,
|
|
753
|
+
position: op.position
|
|
754
|
+
} as BinaryNode;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Right-associative operators (none in FHIRPath currently)
|
|
758
|
+
// For left-associative operators, we want same precedence to associate left
|
|
759
|
+
// So we use precedence + 1 to ensure left-to-right evaluation
|
|
760
|
+
const associativity = this.isRightAssociative(op) ? 0 : 1;
|
|
761
|
+
|
|
762
|
+
let right: ASTNode;
|
|
763
|
+
try {
|
|
764
|
+
right = this.expression(precedence + associativity);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (error instanceof ParseError && (this.diagnostics)) {
|
|
767
|
+
// Report missing operand
|
|
768
|
+
const diagnostic = FHIRPathDiagnostics.missingOperand(op, 'right', this.sourceMapper!);
|
|
769
|
+
this.diagnostics!.addError(diagnostic.range, diagnostic.message, diagnostic.code);
|
|
770
|
+
|
|
771
|
+
if (this.errorRecovery) {
|
|
772
|
+
return this.createIncompleteNode(left, ['right operand']);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
type: NodeType.Binary,
|
|
780
|
+
operator: op.type,
|
|
781
|
+
operation: operation,
|
|
782
|
+
left: left,
|
|
783
|
+
right: right,
|
|
784
|
+
position: op.position
|
|
785
|
+
} as BinaryNode;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Parse function calls
|
|
789
|
+
private functionCall(func: ASTNode): ASTNode {
|
|
790
|
+
const lparen = this.peek();
|
|
791
|
+
|
|
792
|
+
if (!this.match(TokenType.LPAREN)) {
|
|
793
|
+
if (this.errorRecovery) {
|
|
794
|
+
this.errorReporter!.reportExpectedToken(
|
|
795
|
+
[TokenType.LPAREN],
|
|
796
|
+
this.peek(),
|
|
797
|
+
ParseContext.FunctionCall
|
|
798
|
+
);
|
|
799
|
+
return this.createIncompleteNode(func, ['function arguments']);
|
|
800
|
+
}
|
|
801
|
+
throw this.error("Expected '(' after function", ErrorCode.MISSING_ARGUMENTS);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const args: ASTNode[] = [];
|
|
805
|
+
|
|
806
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
807
|
+
const previousContext = this.state.getContext();
|
|
808
|
+
this.state.setContext('FunctionCall');
|
|
809
|
+
// Check for immediate unexpected tokens in function context
|
|
810
|
+
if (this.check(TokenType.RBRACKET) || this.check(TokenType.RBRACE)) {
|
|
811
|
+
if (this.errorRecovery) {
|
|
812
|
+
this.errorReporter!.reportExpectedToken(
|
|
813
|
+
[TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING, TokenType.LPAREN, TokenType.LBRACE],
|
|
814
|
+
this.peek(),
|
|
815
|
+
ParseContext.FunctionCall
|
|
816
|
+
);
|
|
817
|
+
// Skip the unexpected token
|
|
818
|
+
this.advance();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
do {
|
|
823
|
+
try {
|
|
824
|
+
args.push(this.errorRecovery ? this.expressionWithRecovery() : this.expression());
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
827
|
+
// Report contextual error for function arguments
|
|
828
|
+
this.errorReporter!.reportExpectedToken(
|
|
829
|
+
[TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING, TokenType.LPAREN, TokenType.LBRACE],
|
|
830
|
+
error.token,
|
|
831
|
+
ParseContext.FunctionCall
|
|
832
|
+
);
|
|
833
|
+
args.push(this.createErrorNode(error.token, error.message));
|
|
834
|
+
|
|
835
|
+
// Skip to next comma or closing paren
|
|
836
|
+
while (!this.isAtEnd() && !this.check(TokenType.COMMA) && !this.check(TokenType.RPAREN)) {
|
|
837
|
+
this.advance();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (this.check(TokenType.COMMA)) {
|
|
841
|
+
continue;
|
|
842
|
+
} else {
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} while (this.match(TokenType.COMMA));
|
|
850
|
+
|
|
851
|
+
this.state.setContext(previousContext);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Check for empty function that requires arguments
|
|
855
|
+
const funcName = func.type === NodeType.Identifier ? (func as IdentifierNode).name : '';
|
|
856
|
+
if (args.length === 0 && funcName === 'where') {
|
|
857
|
+
if (this.errorRecovery) {
|
|
858
|
+
this.errorReporter!.reportMissingArguments(funcName, lparen);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (!this.match(TokenType.RPAREN)) {
|
|
863
|
+
if (this.errorRecovery) {
|
|
864
|
+
// Report unclosed parenthesis
|
|
865
|
+
this.errorReporter!.reportUnclosedDelimiter(lparen, ')');
|
|
866
|
+
this.state.markPartial();
|
|
867
|
+
} else {
|
|
868
|
+
throw this.error("Expected ')' after arguments", ErrorCode.UNCLOSED_PARENTHESIS);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
let result: ASTNode = {
|
|
873
|
+
type: NodeType.Function,
|
|
874
|
+
name: func,
|
|
875
|
+
arguments: args,
|
|
876
|
+
position: func.position
|
|
877
|
+
} as FunctionNode;
|
|
878
|
+
|
|
879
|
+
// Check for method calls after the function (e.g., exists().not())
|
|
880
|
+
while (this.check(TokenType.DOT)) {
|
|
881
|
+
const dotToken = this.advance();
|
|
882
|
+
|
|
883
|
+
// After a dot, handle keywords that can be method names
|
|
884
|
+
let right: ASTNode;
|
|
885
|
+
const next = this.peek();
|
|
886
|
+
if (this.isOperatorKeyword(next.type)) {
|
|
887
|
+
// Treat keyword as identifier
|
|
888
|
+
this.advance();
|
|
889
|
+
right = {
|
|
890
|
+
type: NodeType.Identifier,
|
|
891
|
+
name: next.value,
|
|
892
|
+
position: next.position
|
|
893
|
+
} as IdentifierNode;
|
|
894
|
+
} else {
|
|
895
|
+
right = this.primary();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// If the right side is a function call, handle it
|
|
899
|
+
if (this.check(TokenType.LPAREN)) {
|
|
900
|
+
const methodNode: BinaryNode = {
|
|
901
|
+
type: NodeType.Binary,
|
|
902
|
+
operator: TokenType.DOT,
|
|
903
|
+
operation: Registry.getByToken(TokenType.DOT),
|
|
904
|
+
left: result,
|
|
905
|
+
right: right,
|
|
906
|
+
position: dotToken.position
|
|
907
|
+
};
|
|
908
|
+
result = this.functionCall(methodNode);
|
|
909
|
+
} else {
|
|
910
|
+
// Regular property access
|
|
911
|
+
result = {
|
|
912
|
+
type: NodeType.Binary,
|
|
913
|
+
operator: TokenType.DOT,
|
|
914
|
+
operation: Registry.getByToken(TokenType.DOT),
|
|
915
|
+
left: result,
|
|
916
|
+
right: right,
|
|
917
|
+
position: dotToken.position
|
|
918
|
+
} as BinaryNode;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Handle indexing
|
|
926
|
+
private parseIndex(expr: ASTNode): ASTNode {
|
|
927
|
+
const lbracket = this.peek();
|
|
928
|
+
|
|
929
|
+
if (!this.match(TokenType.LBRACKET)) {
|
|
930
|
+
throw this.error("Expected '['", ErrorCode.UNEXPECTED_TOKEN);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Check for empty brackets
|
|
934
|
+
if (this.check(TokenType.RBRACKET)) {
|
|
935
|
+
if (this.diagnostics) {
|
|
936
|
+
const diagnostic = FHIRPathDiagnostics.emptyBrackets(lbracket, this.sourceMapper!);
|
|
937
|
+
this.diagnostics!.addError(diagnostic.range, diagnostic.message, diagnostic.code);
|
|
938
|
+
}
|
|
939
|
+
if (this.throwOnError) {
|
|
940
|
+
throw this.error("Expected expression in index", ErrorCode.EXPECTED_EXPRESSION);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
let index: ASTNode;
|
|
945
|
+
try {
|
|
946
|
+
index = this.errorRecovery ? this.expressionWithRecovery() : this.expression();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
949
|
+
index = this.createErrorNode(error.token, error.message);
|
|
950
|
+
} else {
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!this.match(TokenType.RBRACKET)) {
|
|
956
|
+
if (this.diagnostics) {
|
|
957
|
+
this.errorReporter?.reportUnclosedDelimiter(lbracket, ']') ||
|
|
958
|
+
this.diagnostics.addError(
|
|
959
|
+
this.sourceMapper!.tokenToRange(lbracket),
|
|
960
|
+
"Expected ']' after index expression",
|
|
961
|
+
ErrorCode.UNCLOSED_BRACKET
|
|
962
|
+
);
|
|
963
|
+
this.state.markPartial();
|
|
964
|
+
} else {
|
|
965
|
+
throw this.error("Expected ']'", ErrorCode.UNCLOSED_BRACKET);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
type: NodeType.Index,
|
|
971
|
+
expression: expr,
|
|
972
|
+
index: index,
|
|
973
|
+
position: expr.position
|
|
974
|
+
} as IndexNode;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private identifierOrFunctionCall(): ASTNode {
|
|
978
|
+
const name = this.previous().value;
|
|
979
|
+
const position = this.previous().position;
|
|
980
|
+
|
|
981
|
+
// Check if identifier starts with uppercase (potential type)
|
|
982
|
+
const firstChar = name.charAt(0);
|
|
983
|
+
const isUpperCase = firstChar && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
|
|
984
|
+
|
|
985
|
+
const identifier: IdentifierNode | TypeOrIdentifierNode = isUpperCase ? {
|
|
986
|
+
type: NodeType.TypeOrIdentifier,
|
|
987
|
+
name: name,
|
|
988
|
+
position: position
|
|
989
|
+
} : {
|
|
990
|
+
type: NodeType.Identifier,
|
|
991
|
+
name: name,
|
|
992
|
+
position: position
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// Check for function call
|
|
996
|
+
if (this.check(TokenType.LPAREN)) {
|
|
997
|
+
// Special handling for ofType(TypeName)
|
|
998
|
+
if (identifier.name === 'ofType') {
|
|
999
|
+
return this.parseOfType();
|
|
1000
|
+
}
|
|
1001
|
+
// Special handling for is(TypeName) - treat as regular function
|
|
1002
|
+
if (identifier.name === 'is') {
|
|
1003
|
+
return this.functionCall(identifier);
|
|
1004
|
+
}
|
|
1005
|
+
return this.functionCall(identifier);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return identifier;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private parseOfType(): ASTNode {
|
|
1012
|
+
this.consume(TokenType.LPAREN, "Expected '(' after ofType", ErrorCode.MISSING_ARGUMENTS);
|
|
1013
|
+
const typeName = this.consume(TokenType.IDENTIFIER, "Expected type name", ErrorCode.EXPECTED_IDENTIFIER).value;
|
|
1014
|
+
this.consume(TokenType.RPAREN, "Expected ')' after type name");
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
type: NodeType.Function,
|
|
1018
|
+
name: {
|
|
1019
|
+
type: NodeType.Identifier,
|
|
1020
|
+
name: 'ofType',
|
|
1021
|
+
position: this.previous().position
|
|
1022
|
+
} as IdentifierNode,
|
|
1023
|
+
arguments: [{
|
|
1024
|
+
type: NodeType.TypeReference,
|
|
1025
|
+
typeName: typeName,
|
|
1026
|
+
position: this.previous().position
|
|
1027
|
+
} as TypeReferenceNode],
|
|
1028
|
+
position: this.previous().position
|
|
1029
|
+
} as FunctionNode;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Precedence lookup is now handled by PrecedenceManager
|
|
1033
|
+
|
|
1034
|
+
// Type inference is now handled by ASTFactory.inferLiteralType
|
|
1035
|
+
|
|
1036
|
+
// Helper methods
|
|
1037
|
+
private match(...types: TokenType[]): boolean {
|
|
1038
|
+
return this.navigator.match(...types);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private check(type: TokenType): boolean {
|
|
1042
|
+
return this.navigator.check(type);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private advance(): Token {
|
|
1046
|
+
return this.navigator.advance();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private isAtEnd(): boolean {
|
|
1050
|
+
return this.navigator.isAtEnd();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private peek(): Token {
|
|
1054
|
+
return this.navigator.peek();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private previous(): Token {
|
|
1058
|
+
return this.navigator.previous();
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private consume(type: TokenType, message: string, code: ErrorCode = ErrorCode.UNEXPECTED_TOKEN): Token {
|
|
1062
|
+
if (this.check(type)) return this.advance();
|
|
1063
|
+
throw this.error(message, code);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private error(message: string, code: ErrorCode = ErrorCode.PARSE_ERROR): ParseError {
|
|
1067
|
+
const token = this.peek();
|
|
1068
|
+
return this.errorFactory.createError(message, token, code);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private isRightAssociative(op: Token): boolean {
|
|
1072
|
+
// FHIRPath doesn't have right-associative operators
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Check if a token type is an operator keyword that can also be identifier/function
|
|
1077
|
+
private isOperatorKeyword(type: TokenType): boolean {
|
|
1078
|
+
return type === TokenType.DIV ||
|
|
1079
|
+
type === TokenType.MOD ||
|
|
1080
|
+
type === TokenType.CONTAINS ||
|
|
1081
|
+
type === TokenType.IN ||
|
|
1082
|
+
type === TokenType.AND ||
|
|
1083
|
+
type === TokenType.OR ||
|
|
1084
|
+
type === TokenType.XOR ||
|
|
1085
|
+
type === TokenType.IMPLIES ||
|
|
1086
|
+
type === TokenType.IS ||
|
|
1087
|
+
type === TokenType.AS ||
|
|
1088
|
+
type === TokenType.NOT ||
|
|
1089
|
+
type === TokenType.TRUE ||
|
|
1090
|
+
type === TokenType.FALSE;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Error recovery methods
|
|
1094
|
+
private expressionWithRecovery(minPrecedence: number = 0): ASTNode {
|
|
1095
|
+
try {
|
|
1096
|
+
return this.expression(minPrecedence);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
1099
|
+
const errorNode = this.createErrorNode(error.token, error.message);
|
|
1100
|
+
this.recoverToSyncPoint();
|
|
1101
|
+
|
|
1102
|
+
// Try to continue parsing after recovery
|
|
1103
|
+
if (!this.isAtEnd() && !this.isAtSyncPoint()) {
|
|
1104
|
+
// Only try to recover if we're not already at a sync point
|
|
1105
|
+
try {
|
|
1106
|
+
const recovered = this.expression(minPrecedence); // Use regular expression, not recursive recovery
|
|
1107
|
+
// Create a binary node with error on left
|
|
1108
|
+
return {
|
|
1109
|
+
type: NodeType.Binary,
|
|
1110
|
+
operator: TokenType.DOT,
|
|
1111
|
+
left: errorNode,
|
|
1112
|
+
right: recovered,
|
|
1113
|
+
position: errorNode.position
|
|
1114
|
+
} as BinaryNode;
|
|
1115
|
+
} catch {
|
|
1116
|
+
// If recovery fails, just return the error node
|
|
1117
|
+
return errorNode;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return errorNode;
|
|
1122
|
+
}
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private primaryWithRecovery(): ASTNode {
|
|
1128
|
+
try {
|
|
1129
|
+
return this.primary();
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (error instanceof ParseError && this.errorRecovery) {
|
|
1132
|
+
const errorNode = this.createErrorNode(error.token, error.message);
|
|
1133
|
+
this.recoverToSyncPoint();
|
|
1134
|
+
return errorNode;
|
|
1135
|
+
}
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private createErrorNode(token: Token, message: string): ErrorNode {
|
|
1141
|
+
const range = this.sourceMapper!.tokenToRange(token);
|
|
1142
|
+
const diagnostic = this.diagnostics!.getDiagnostics().slice(-1)[0];
|
|
1143
|
+
|
|
1144
|
+
this.state.markPartial();
|
|
1145
|
+
|
|
1146
|
+
return {
|
|
1147
|
+
type: NodeType.Error,
|
|
1148
|
+
position: token.position,
|
|
1149
|
+
range,
|
|
1150
|
+
expectedTokens: [], // Could be enhanced to track expected tokens
|
|
1151
|
+
actualToken: token,
|
|
1152
|
+
diagnostic: diagnostic || {
|
|
1153
|
+
range,
|
|
1154
|
+
severity: 1,
|
|
1155
|
+
code: ErrorCode.PARSE_ERROR,
|
|
1156
|
+
message,
|
|
1157
|
+
source: 'fhirpath-parser'
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private createIncompleteNode(partialNode: ASTNode | undefined, missingParts: string[]): IncompleteNode {
|
|
1163
|
+
const position = partialNode?.position || this.peek().position;
|
|
1164
|
+
const range = partialNode?.range || this.sourceMapper!.tokenToRange(this.peek());
|
|
1165
|
+
|
|
1166
|
+
this.state.markPartial();
|
|
1167
|
+
|
|
1168
|
+
return {
|
|
1169
|
+
type: NodeType.Incomplete,
|
|
1170
|
+
position,
|
|
1171
|
+
range,
|
|
1172
|
+
partialNode,
|
|
1173
|
+
missingParts
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private recoverToSyncPoint(): void {
|
|
1178
|
+
while (!this.isAtEnd() && !this.isAtSyncPoint()) {
|
|
1179
|
+
this.advance();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
private isAtSyncPoint(): boolean {
|
|
1184
|
+
const token = this.peek();
|
|
1185
|
+
return token.type === TokenType.COMMA ||
|
|
1186
|
+
token.type === TokenType.RPAREN ||
|
|
1187
|
+
token.type === TokenType.RBRACKET ||
|
|
1188
|
+
token.type === TokenType.RBRACE ||
|
|
1189
|
+
token.type === TokenType.PIPE ||
|
|
1190
|
+
token.type === TokenType.AND ||
|
|
1191
|
+
token.type === TokenType.OR ||
|
|
1192
|
+
this.isStatementBoundary(token);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private isStatementBoundary(token: Token): boolean {
|
|
1196
|
+
// For future multi-statement support
|
|
1197
|
+
return token.type === TokenType.EOF;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Synchronization points for error recovery
|
|
1201
|
+
private synchronize() {
|
|
1202
|
+
while (!this.isAtEnd()) {
|
|
1203
|
+
if (this.previous().type === TokenType.COMMA) return;
|
|
1204
|
+
if (this.previous().type === TokenType.RPAREN) return;
|
|
1205
|
+
|
|
1206
|
+
switch (this.peek().type) {
|
|
1207
|
+
case TokenType.IDENTIFIER:
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
this.advance();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Add helper method to collect node ranges
|
|
1216
|
+
private collectNodeRanges(ast: ASTNode): Map<ASTNode, TextRange> {
|
|
1217
|
+
const ranges = new Map<ASTNode, TextRange>();
|
|
1218
|
+
|
|
1219
|
+
const visit = (node: ASTNode) => {
|
|
1220
|
+
if (this.sourceMapper && node.position) {
|
|
1221
|
+
// Create range from node position
|
|
1222
|
+
const range: TextRange = {
|
|
1223
|
+
start: {
|
|
1224
|
+
line: node.position.line - 1,
|
|
1225
|
+
character: node.position.column - 1,
|
|
1226
|
+
offset: node.position.offset
|
|
1227
|
+
},
|
|
1228
|
+
end: {
|
|
1229
|
+
line: node.position.line - 1,
|
|
1230
|
+
character: node.position.column - 1,
|
|
1231
|
+
offset: node.position.offset
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
ranges.set(node, range);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Visit child nodes based on node type
|
|
1238
|
+
switch (node.type) {
|
|
1239
|
+
case NodeType.Binary:
|
|
1240
|
+
visit((node as BinaryNode).left);
|
|
1241
|
+
visit((node as BinaryNode).right);
|
|
1242
|
+
break;
|
|
1243
|
+
case NodeType.Unary:
|
|
1244
|
+
visit((node as UnaryNode).operand);
|
|
1245
|
+
break;
|
|
1246
|
+
case NodeType.Function:
|
|
1247
|
+
(node as FunctionNode).arguments.forEach(visit);
|
|
1248
|
+
break;
|
|
1249
|
+
case NodeType.Index:
|
|
1250
|
+
visit((node as IndexNode).expression);
|
|
1251
|
+
visit((node as IndexNode).index);
|
|
1252
|
+
break;
|
|
1253
|
+
case NodeType.Union:
|
|
1254
|
+
(node as UnionNode).operands.forEach(visit);
|
|
1255
|
+
break;
|
|
1256
|
+
case NodeType.Collection:
|
|
1257
|
+
(node as CollectionNode).elements.forEach(visit);
|
|
1258
|
+
break;
|
|
1259
|
+
case NodeType.MembershipTest:
|
|
1260
|
+
visit((node as MembershipTestNode).expression);
|
|
1261
|
+
break;
|
|
1262
|
+
case NodeType.TypeCast:
|
|
1263
|
+
visit((node as TypeCastNode).expression);
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
visit(ast);
|
|
1269
|
+
return ranges;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|