@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.
Files changed (85) hide show
  1. package/README.md +473 -0
  2. package/dist/index.d.ts +462 -0
  3. package/dist/index.js +10307 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +58 -0
  6. package/src/analyzer/analyzer.ts +499 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +157 -0
  12. package/src/api/errors.ts +145 -0
  13. package/src/api/expression.ts +156 -0
  14. package/src/api/index.ts +122 -0
  15. package/src/api/inspect.ts +99 -0
  16. package/src/api/registry.ts +128 -0
  17. package/src/api/types.ts +210 -0
  18. package/src/compiler/compiler.ts +546 -0
  19. package/src/compiler/index.ts +2 -0
  20. package/src/compiler/prototype-context-adapter.ts +99 -0
  21. package/src/compiler/types.ts +24 -0
  22. package/src/index.ts +107 -0
  23. package/src/interpreter/README.md +78 -0
  24. package/src/interpreter/interpreter.ts +475 -0
  25. package/src/interpreter/types.ts +108 -0
  26. package/src/lexer/char-tables.ts +37 -0
  27. package/src/lexer/errors.ts +31 -0
  28. package/src/lexer/index.ts +5 -0
  29. package/src/lexer/lexer.ts +745 -0
  30. package/src/lexer/token.ts +104 -0
  31. package/src/lexer2/index.md +232 -0
  32. package/src/lexer2/index.perf.test.ts +68 -0
  33. package/src/lexer2/index.test.ts +549 -0
  34. package/src/lexer2/index.ts +1251 -0
  35. package/src/lexer2/notes.md +173 -0
  36. package/src/lexer2/optimization-summary.md +718 -0
  37. package/src/parser/ast-factory.ts +220 -0
  38. package/src/parser/ast.ts +144 -0
  39. package/src/parser/collection-parser.ts +89 -0
  40. package/src/parser/diagnostic-messages.ts +216 -0
  41. package/src/parser/diagnostics.ts +85 -0
  42. package/src/parser/error-reporter.ts +230 -0
  43. package/src/parser/index.ts +3 -0
  44. package/src/parser/literal-parser.ts +103 -0
  45. package/src/parser/parse-error.ts +16 -0
  46. package/src/parser/parser-error-factory.ts +141 -0
  47. package/src/parser/parser-state.ts +134 -0
  48. package/src/parser/parser.ts +1272 -0
  49. package/src/parser/pprint.ts +169 -0
  50. package/src/parser/precedence-manager.ts +64 -0
  51. package/src/parser/source-mapper.ts +248 -0
  52. package/src/parser/special-constructs.ts +142 -0
  53. package/src/parser/token-navigator.ts +110 -0
  54. package/src/parser/types.ts +60 -0
  55. package/src/parser2/index.md +177 -0
  56. package/src/parser2/index.perf.test.ts +184 -0
  57. package/src/parser2/index.test.ts +305 -0
  58. package/src/parser2/index.ts +578 -0
  59. package/src/parser2/optimization-summary.md +176 -0
  60. package/src/registry/default-analyzers.ts +257 -0
  61. package/src/registry/default-compilers.ts +31 -0
  62. package/src/registry/index.ts +96 -0
  63. package/src/registry/operations/arithmetic.ts +506 -0
  64. package/src/registry/operations/collection.ts +425 -0
  65. package/src/registry/operations/comparison.ts +432 -0
  66. package/src/registry/operations/existence.ts +703 -0
  67. package/src/registry/operations/filtering.ts +358 -0
  68. package/src/registry/operations/literals.ts +341 -0
  69. package/src/registry/operations/logical.ts +439 -0
  70. package/src/registry/operations/math.ts +128 -0
  71. package/src/registry/operations/membership.ts +132 -0
  72. package/src/registry/operations/navigation.ts +52 -0
  73. package/src/registry/operations/string.ts +507 -0
  74. package/src/registry/operations/subsetting.ts +174 -0
  75. package/src/registry/operations/type-checking.ts +162 -0
  76. package/src/registry/operations/type-conversion.ts +404 -0
  77. package/src/registry/operations/type-operators.ts +308 -0
  78. package/src/registry/operations/utility.ts +644 -0
  79. package/src/registry/registry.ts +146 -0
  80. package/src/registry/types.ts +161 -0
  81. package/src/registry/utils/evaluation-helpers.ts +93 -0
  82. package/src/registry/utils/index.ts +3 -0
  83. package/src/registry/utils/type-system.ts +173 -0
  84. package/src/runtime/context.ts +158 -0
  85. 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
+