@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800

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 (57) hide show
  1. package/README.md +307 -0
  2. package/dist/index.d.ts +225 -0
  3. package/dist/index.js +8185 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/analyzer/analyzer.ts +486 -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 +148 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +152 -0
  14. package/src/api/index.ts +57 -0
  15. package/src/api/registry.ts +128 -0
  16. package/src/api/types.ts +154 -0
  17. package/src/compiler/compiler.ts +579 -0
  18. package/src/compiler/index.ts +2 -0
  19. package/src/compiler/prototype-context-adapter.ts +99 -0
  20. package/src/compiler/types.ts +23 -0
  21. package/src/index.ts +52 -0
  22. package/src/interpreter/README.md +78 -0
  23. package/src/interpreter/interpreter.ts +485 -0
  24. package/src/interpreter/types.ts +110 -0
  25. package/src/lexer/char-tables.ts +37 -0
  26. package/src/lexer/errors.ts +31 -0
  27. package/src/lexer/index.ts +5 -0
  28. package/src/lexer/lexer.ts +745 -0
  29. package/src/lexer/token.ts +104 -0
  30. package/src/parser/ast.ts +123 -0
  31. package/src/parser/index.ts +3 -0
  32. package/src/parser/parser.ts +701 -0
  33. package/src/parser/pprint.ts +169 -0
  34. package/src/registry/default-analyzers.ts +257 -0
  35. package/src/registry/default-compilers.ts +31 -0
  36. package/src/registry/index.ts +93 -0
  37. package/src/registry/operations/arithmetic.ts +506 -0
  38. package/src/registry/operations/collection.ts +425 -0
  39. package/src/registry/operations/comparison.ts +432 -0
  40. package/src/registry/operations/existence.ts +703 -0
  41. package/src/registry/operations/filtering.ts +358 -0
  42. package/src/registry/operations/literals.ts +341 -0
  43. package/src/registry/operations/logical.ts +402 -0
  44. package/src/registry/operations/math.ts +128 -0
  45. package/src/registry/operations/membership.ts +132 -0
  46. package/src/registry/operations/string.ts +507 -0
  47. package/src/registry/operations/subsetting.ts +174 -0
  48. package/src/registry/operations/type-checking.ts +162 -0
  49. package/src/registry/operations/type-conversion.ts +404 -0
  50. package/src/registry/operations/type-operators.ts +307 -0
  51. package/src/registry/operations/utility.ts +542 -0
  52. package/src/registry/registry.ts +146 -0
  53. package/src/registry/types.ts +161 -0
  54. package/src/registry/utils/evaluation-helpers.ts +93 -0
  55. package/src/registry/utils/index.ts +3 -0
  56. package/src/registry/utils/type-system.ts +173 -0
  57. package/src/runtime/context.ts +179 -0
@@ -0,0 +1,701 @@
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
+ NodeType
21
+ } from './ast';
22
+ import { Registry } from '../registry';
23
+
24
+ export class ParseError extends Error {
25
+ constructor(
26
+ message: string,
27
+ public position: Position,
28
+ public token: Token
29
+ ) {
30
+ super(message);
31
+ }
32
+ }
33
+
34
+ // Precedence levels for reference (from spec)
35
+ // INVOCATION: 1, // . (dot), function calls
36
+ // POSTFIX: 2, // [] indexing
37
+ // UNARY: 3, // unary +, -, not
38
+ // MULTIPLICATIVE: 4, // *, /, div, mod
39
+ // ADDITIVE: 5, // +, -, &
40
+ // TYPE: 6, // is, as
41
+ // UNION: 7, // |
42
+ // RELATIONAL: 8, // <, >, <=, >=
43
+ // EQUALITY: 9, // =, ~, !=, !~
44
+ // MEMBERSHIP: 10, // in, contains
45
+ // AND: 11, // and
46
+ // OR: 12, // or, xor
47
+ // IMPLIES: 13, // implies
48
+
49
+ export class FHIRPathParser {
50
+ private tokens: Token[];
51
+ private current: number = 0;
52
+
53
+ constructor(input: string | Token[]) {
54
+ if (typeof input === 'string') {
55
+ this.tokens = lex(input); // Assuming lex function from lexer
56
+ } else {
57
+ this.tokens = input;
58
+ }
59
+ this.current = 0;
60
+ }
61
+
62
+ // Main entry point
63
+ parse(): ASTNode {
64
+ const ast = this.expression();
65
+ if (!this.isAtEnd()) {
66
+ throw this.error("Unexpected token after expression");
67
+ }
68
+ return ast;
69
+ }
70
+
71
+ // Pratt parser for expressions
72
+ private expression(minPrecedence: number = 14): ASTNode {
73
+ let left = this.primary();
74
+
75
+ while (!this.isAtEnd()) {
76
+ // Handle postfix operators first
77
+ if (this.check(TokenType.LBRACKET) && minPrecedence >= 2) { // POSTFIX precedence
78
+ left = this.parseIndex(left);
79
+ continue;
80
+ }
81
+
82
+ // Handle function calls that come from primary() or after dots
83
+ // This allows for chained method calls like exists().not()
84
+ if (this.check(TokenType.DOT) && minPrecedence >= 1) { // INVOCATION precedence
85
+ const dotToken = this.peek();
86
+ const precedence = this.getPrecedence(dotToken);
87
+ if (precedence > minPrecedence) break;
88
+
89
+ left = this.parseBinary(left, dotToken, precedence);
90
+ continue;
91
+ }
92
+
93
+ const token = this.peek();
94
+ const precedence = this.getPrecedence(token);
95
+
96
+ if (precedence === 0 || precedence > minPrecedence) break;
97
+
98
+ left = this.parseBinary(left, token, precedence);
99
+ }
100
+
101
+ return left;
102
+ }
103
+
104
+ // Parse primary expressions (recursive descent)
105
+ private primary(): ASTNode {
106
+ // Handle registry-based literals
107
+ if (this.match(TokenType.LITERAL)) {
108
+ const token = this.previous();
109
+ return {
110
+ type: NodeType.Literal,
111
+ value: token.literalValue ?? token.value,
112
+ valueType: this.inferLiteralType(token.literalValue ?? token.value),
113
+ raw: token.value,
114
+ operation: token.operation,
115
+ position: token.position
116
+ } as LiteralNode;
117
+ }
118
+
119
+ // Handle legacy literals
120
+ if (this.match(TokenType.NUMBER)) {
121
+ const token = this.previous();
122
+ return {
123
+ type: NodeType.Literal,
124
+ value: parseFloat(token.value),
125
+ valueType: 'number',
126
+ position: token.position
127
+ } as LiteralNode;
128
+ }
129
+ if (this.match(TokenType.STRING)) {
130
+ const token = this.previous();
131
+ return {
132
+ type: NodeType.Literal,
133
+ value: token.value,
134
+ valueType: 'string',
135
+ position: token.position
136
+ } as LiteralNode;
137
+ }
138
+ if (this.match(TokenType.TRUE, TokenType.FALSE)) {
139
+ const token = this.previous();
140
+ return {
141
+ type: NodeType.Literal,
142
+ value: token.type === TokenType.TRUE,
143
+ valueType: 'boolean',
144
+ position: token.position
145
+ } as LiteralNode;
146
+ }
147
+ if (this.match(TokenType.NULL)) {
148
+ return {
149
+ type: NodeType.Literal,
150
+ value: null,
151
+ valueType: 'null',
152
+ position: this.previous().position
153
+ } as LiteralNode;
154
+ }
155
+
156
+ // Handle variables
157
+ if (this.match(TokenType.THIS)) {
158
+ return {
159
+ type: NodeType.Variable,
160
+ name: '$this',
161
+ position: this.previous().position
162
+ } as VariableNode;
163
+ }
164
+ if (this.match(TokenType.INDEX)) {
165
+ return {
166
+ type: NodeType.Variable,
167
+ name: '$index',
168
+ position: this.previous().position
169
+ } as VariableNode;
170
+ }
171
+ if (this.match(TokenType.TOTAL)) {
172
+ return {
173
+ type: NodeType.Variable,
174
+ name: '$total',
175
+ position: this.previous().position
176
+ } as VariableNode;
177
+ }
178
+ if (this.match(TokenType.ENV_VAR)) {
179
+ return {
180
+ type: NodeType.Variable,
181
+ name: this.previous().value,
182
+ position: this.previous().position
183
+ } as VariableNode;
184
+ }
185
+
186
+ // Handle dates/times
187
+ if (this.match(TokenType.DATE, TokenType.DATETIME, TokenType.TIME)) {
188
+ const token = this.previous();
189
+ return {
190
+ type: NodeType.Literal,
191
+ value: token.value,
192
+ valueType: token.type === TokenType.DATE ? 'date' :
193
+ token.type === TokenType.TIME ? 'time' : 'datetime',
194
+ position: token.position
195
+ } as LiteralNode;
196
+ }
197
+
198
+ // Handle grouping
199
+ if (this.match(TokenType.LPAREN)) {
200
+ const expr = this.expression();
201
+ this.consume(TokenType.RPAREN, "Expected ')' after expression");
202
+
203
+ // Check for method calls after parentheses (e.g., (expr).method())
204
+ let result = expr;
205
+ while (this.check(TokenType.DOT)) {
206
+ const dotToken = this.advance();
207
+
208
+ // After a dot, handle keywords that can be method names
209
+ let right: ASTNode;
210
+ const next = this.peek();
211
+ if (this.isOperatorKeyword(next.type)) {
212
+ // Treat keyword as identifier
213
+ this.advance();
214
+ right = {
215
+ type: NodeType.Identifier,
216
+ name: next.value,
217
+ position: next.position
218
+ } as IdentifierNode;
219
+ } else {
220
+ right = this.primary();
221
+ }
222
+
223
+ // If the right side is a function call, handle it
224
+ if (this.check(TokenType.LPAREN)) {
225
+ const methodNode: BinaryNode = {
226
+ type: NodeType.Binary,
227
+ operator: TokenType.DOT,
228
+ operation: Registry.getByToken(TokenType.DOT),
229
+ left: result,
230
+ right: right,
231
+ position: dotToken.position
232
+ };
233
+ result = this.functionCall(methodNode);
234
+ } else {
235
+ // Regular property access
236
+ result = {
237
+ type: NodeType.Binary,
238
+ operator: TokenType.DOT,
239
+ operation: Registry.getByToken(TokenType.DOT),
240
+ left: result,
241
+ right: right,
242
+ position: dotToken.position
243
+ } as BinaryNode;
244
+ }
245
+ }
246
+
247
+ return result;
248
+ }
249
+
250
+ // Handle identifiers (which might be followed by function calls)
251
+ if (this.match(TokenType.IDENTIFIER)) {
252
+ return this.identifierOrFunctionCall();
253
+ }
254
+
255
+ // Handle delimited identifiers
256
+ if (this.match(TokenType.DELIMITED_IDENTIFIER)) {
257
+ return this.identifierOrFunctionCall();
258
+ }
259
+
260
+ // Handle unary operators
261
+ if (this.match(TokenType.PLUS, TokenType.MINUS, TokenType.NOT)) {
262
+ const op = this.previous();
263
+ const operation = op.operation || Registry.getByToken(op.type);
264
+ const right = this.expression(3); // UNARY precedence
265
+ return {
266
+ type: NodeType.Unary,
267
+ operator: op.type,
268
+ operation: operation,
269
+ operand: right,
270
+ position: op.position
271
+ } as UnaryNode;
272
+ }
273
+
274
+ // Handle empty collection {} or collection literals {expr1, expr2, ...}
275
+ if (this.match(TokenType.LBRACE)) {
276
+ const startPos = this.previous().position;
277
+ const elements: ASTNode[] = [];
278
+
279
+ if (!this.check(TokenType.RBRACE)) {
280
+ do {
281
+ elements.push(this.expression());
282
+ } while (this.match(TokenType.COMMA));
283
+ }
284
+
285
+ this.consume(TokenType.RBRACE, "Expected '}' after collection elements");
286
+ return {
287
+ type: NodeType.Collection,
288
+ elements: elements,
289
+ position: startPos
290
+ } as CollectionNode;
291
+ }
292
+
293
+ // Handle operator keywords as identifiers/functions at expression start
294
+ if (this.isOperatorKeyword(this.peek().type)) {
295
+ const token = this.advance();
296
+ const identifier: IdentifierNode = {
297
+ type: NodeType.Identifier,
298
+ name: token.value,
299
+ position: token.position
300
+ };
301
+
302
+ // Check for function call
303
+ if (this.check(TokenType.LPAREN)) {
304
+ return this.functionCall(identifier);
305
+ }
306
+
307
+ return identifier;
308
+ }
309
+
310
+ throw this.error("Expected expression");
311
+ }
312
+
313
+ // Parse binary operators with precedence
314
+ private parseBinary(left: ASTNode, op: Token, precedence: number): ASTNode {
315
+ const operation = op.operation || Registry.getByToken(op.type);
316
+ if (!operation && op.type !== TokenType.DOT && op.type !== TokenType.IS && op.type !== TokenType.AS) {
317
+ throw this.error(`Unknown operator: ${op.value}`);
318
+ }
319
+ // Special handling for type operators
320
+ if (op.type === TokenType.IS || op.type === TokenType.AS) {
321
+ this.advance(); // consume operator
322
+
323
+ // Type name can be either a simple identifier or in parentheses (for is() function syntax)
324
+ let typeName: string;
325
+ if (this.check(TokenType.LPAREN)) {
326
+ // Handle is(TypeName) syntax
327
+ this.advance(); // consume (
328
+ typeName = this.consume(TokenType.IDENTIFIER, "Expected type name").value;
329
+ this.consume(TokenType.RPAREN, "Expected ')' after type name");
330
+ } else {
331
+ // Regular is TypeName syntax
332
+ typeName = this.consume(TokenType.IDENTIFIER, "Expected type name").value;
333
+ }
334
+
335
+ return {
336
+ type: op.type === TokenType.IS ? NodeType.MembershipTest : NodeType.TypeCast,
337
+ expression: left,
338
+ targetType: typeName,
339
+ position: op.position
340
+ } as MembershipTestNode | TypeCastNode;
341
+ }
342
+
343
+ this.advance(); // consume operator
344
+
345
+ // Special handling for union operator - can chain multiple
346
+ if (op.type === TokenType.PIPE) {
347
+ const right = this.expression(precedence - 1);
348
+
349
+ // If left is already a union, add to it
350
+ if (left.type === NodeType.Union) {
351
+ (left as UnionNode).operands.push(right);
352
+ return left;
353
+ }
354
+
355
+ // Create new union node
356
+ return {
357
+ type: NodeType.Union,
358
+ operands: [left, right],
359
+ position: op.position
360
+ } as UnionNode;
361
+ }
362
+
363
+ // Special handling for dot operator (left-associative, pipelines data)
364
+ if (op.type === TokenType.DOT) {
365
+ // After a dot, we need to handle keywords that can be method names
366
+ let right: ASTNode;
367
+
368
+ // Check if next token is a keyword that can be used as a method name
369
+ const next = this.peek();
370
+ if (this.isOperatorKeyword(next.type)) {
371
+ // Treat keyword as identifier
372
+ this.advance();
373
+ right = {
374
+ type: NodeType.Identifier,
375
+ name: next.value,
376
+ position: next.position
377
+ } as IdentifierNode;
378
+ } else {
379
+ right = this.primary();
380
+ }
381
+
382
+ // Check for function call after dot
383
+ if (this.peek().type === TokenType.LPAREN) {
384
+ const dotNode: BinaryNode = {
385
+ type: NodeType.Binary,
386
+ operator: TokenType.DOT,
387
+ operation: operation,
388
+ left: left,
389
+ right: right,
390
+ position: op.position
391
+ };
392
+ return this.functionCall(dotNode);
393
+ }
394
+
395
+ return {
396
+ type: NodeType.Binary,
397
+ operator: TokenType.DOT,
398
+ operation: operation,
399
+ left: left,
400
+ right: right,
401
+ position: op.position
402
+ } as BinaryNode;
403
+ }
404
+
405
+ // Right-associative operators (none in FHIRPath currently)
406
+ const associativity = this.isRightAssociative(op) ? 0 : -1;
407
+ const right = this.expression(precedence + associativity);
408
+
409
+ return {
410
+ type: NodeType.Binary,
411
+ operator: op.type,
412
+ operation: operation,
413
+ left: left,
414
+ right: right,
415
+ position: op.position
416
+ } as BinaryNode;
417
+ }
418
+
419
+ // Parse function calls
420
+ private functionCall(func: ASTNode): ASTNode {
421
+ this.consume(TokenType.LPAREN, "Expected '(' after function");
422
+
423
+ const args: ASTNode[] = [];
424
+
425
+ if (!this.check(TokenType.RPAREN)) {
426
+ do {
427
+ args.push(this.expression());
428
+ } while (this.match(TokenType.COMMA));
429
+ }
430
+
431
+ this.consume(TokenType.RPAREN, "Expected ')' after arguments");
432
+
433
+ let result: ASTNode = {
434
+ type: NodeType.Function,
435
+ name: func,
436
+ arguments: args,
437
+ position: func.position
438
+ } as FunctionNode;
439
+
440
+ // Check for method calls after the function (e.g., exists().not())
441
+ while (this.check(TokenType.DOT)) {
442
+ const dotToken = this.advance();
443
+
444
+ // After a dot, handle keywords that can be method names
445
+ let right: ASTNode;
446
+ const next = this.peek();
447
+ if (this.isOperatorKeyword(next.type)) {
448
+ // Treat keyword as identifier
449
+ this.advance();
450
+ right = {
451
+ type: NodeType.Identifier,
452
+ name: next.value,
453
+ position: next.position
454
+ } as IdentifierNode;
455
+ } else {
456
+ right = this.primary();
457
+ }
458
+
459
+ // If the right side is a function call, handle it
460
+ if (this.check(TokenType.LPAREN)) {
461
+ const methodNode: BinaryNode = {
462
+ type: NodeType.Binary,
463
+ operator: TokenType.DOT,
464
+ operation: Registry.getByToken(TokenType.DOT),
465
+ left: result,
466
+ right: right,
467
+ position: dotToken.position
468
+ };
469
+ result = this.functionCall(methodNode);
470
+ } else {
471
+ // Regular property access
472
+ result = {
473
+ type: NodeType.Binary,
474
+ operator: TokenType.DOT,
475
+ operation: Registry.getByToken(TokenType.DOT),
476
+ left: result,
477
+ right: right,
478
+ position: dotToken.position
479
+ } as BinaryNode;
480
+ }
481
+ }
482
+
483
+ return result;
484
+ }
485
+
486
+ // Handle indexing
487
+ private parseIndex(expr: ASTNode): ASTNode {
488
+ this.consume(TokenType.LBRACKET, "Expected '['");
489
+ const index = this.expression();
490
+ this.consume(TokenType.RBRACKET, "Expected ']'");
491
+
492
+ return {
493
+ type: NodeType.Index,
494
+ expression: expr,
495
+ index: index,
496
+ position: expr.position
497
+ } as IndexNode;
498
+ }
499
+
500
+ private identifierOrFunctionCall(): ASTNode {
501
+ const name = this.previous().value;
502
+ const position = this.previous().position;
503
+
504
+ // Check if identifier starts with uppercase (potential type)
505
+ const firstChar = name.charAt(0);
506
+ const isUpperCase = firstChar && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
507
+
508
+ const identifier: IdentifierNode | TypeOrIdentifierNode = isUpperCase ? {
509
+ type: NodeType.TypeOrIdentifier,
510
+ name: name,
511
+ position: position
512
+ } : {
513
+ type: NodeType.Identifier,
514
+ name: name,
515
+ position: position
516
+ };
517
+
518
+ // Check for function call
519
+ if (this.check(TokenType.LPAREN)) {
520
+ // Special handling for ofType(TypeName)
521
+ if (identifier.name === 'ofType') {
522
+ return this.parseOfType();
523
+ }
524
+ // Special handling for is(TypeName) - treat as regular function
525
+ if (identifier.name === 'is') {
526
+ return this.functionCall(identifier);
527
+ }
528
+ return this.functionCall(identifier);
529
+ }
530
+
531
+ return identifier;
532
+ }
533
+
534
+ private parseOfType(): ASTNode {
535
+ this.consume(TokenType.LPAREN, "Expected '(' after ofType");
536
+ const typeName = this.consume(TokenType.IDENTIFIER, "Expected type name").value;
537
+ this.consume(TokenType.RPAREN, "Expected ')' after type name");
538
+
539
+ return {
540
+ type: NodeType.Function,
541
+ name: {
542
+ type: NodeType.Identifier,
543
+ name: 'ofType',
544
+ position: this.previous().position
545
+ } as IdentifierNode,
546
+ arguments: [{
547
+ type: NodeType.TypeReference,
548
+ typeName: typeName,
549
+ position: this.previous().position
550
+ } as TypeReferenceNode],
551
+ position: this.previous().position
552
+ } as FunctionNode;
553
+ }
554
+
555
+ // Precedence lookup (high precedence = low number)
556
+ private getPrecedence(token: Token): number {
557
+ // Special case for DOT which might not be in registry yet
558
+ if (token.type === TokenType.DOT) return 1;
559
+
560
+ // Use registry for all other operators
561
+ const registryPrecedence = Registry.getPrecedence(token.type);
562
+
563
+ // Registry uses standard convention (higher number = higher precedence)
564
+ // Parser uses inverted convention (lower number = higher precedence)
565
+ // So we need to invert the value
566
+ if (registryPrecedence === 0) return 0; // No precedence
567
+
568
+ // The Registry precedence values seem to be inverted from FHIRPath spec
569
+ // We need to map them correctly:
570
+ // Registry -> Parser (lower is higher precedence)
571
+ // 1 (implies) -> 13
572
+ // 2 (or) -> 12
573
+ // 3 (and) -> 11
574
+ // 5 (additive) -> 5
575
+ // 6 (multiplicative, type) -> 4
576
+ // 8 (relational) -> 8
577
+ // 9 (equality) -> 9
578
+ // 10 (membership, unary) -> 10 or 3
579
+ // 13 (union) -> 7
580
+
581
+ // For now, use the simple inversion but adjust for proper ordering
582
+ // Multiplicative (6) should have higher precedence than comparison (8-9)
583
+ // So we need a different mapping
584
+ const precedenceMap: Record<number, number> = {
585
+ 1: 13, // implies - lowest
586
+ 2: 12, // or, xor
587
+ 3: 11, // and
588
+ 5: 5, // additive (+, -, &)
589
+ 6: 4, // multiplicative (*, /, div, mod) and type (is, as)
590
+ 8: 8, // relational (<, >, <=, >=)
591
+ 9: 9, // equality (=, !=, ~, !~)
592
+ 10: 10, // membership (in, contains) - but unary should be 3
593
+ 13: 7 // union (|)
594
+ };
595
+
596
+ return precedenceMap[registryPrecedence] ?? (15 - registryPrecedence);
597
+ }
598
+
599
+ // Helper to infer literal type from value
600
+ private inferLiteralType(value: any): 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null' {
601
+ if (value === null) return 'null';
602
+ if (typeof value === 'boolean') return 'boolean';
603
+ if (typeof value === 'number') return 'number';
604
+ if (typeof value === 'string') return 'string';
605
+ if (value instanceof Date) {
606
+ // Check if it has time component
607
+ const hasTime = value.getHours() !== 0 || value.getMinutes() !== 0 || value.getSeconds() !== 0;
608
+ return hasTime ? 'datetime' : 'date';
609
+ }
610
+ // Check for time-only values (stored as strings like "14:30:00")
611
+ if (typeof value === 'object' && value.type === 'time') return 'time';
612
+ return 'string'; // default
613
+ }
614
+
615
+ // Helper methods
616
+ private match(...types: TokenType[]): boolean {
617
+ for (const type of types) {
618
+ if (this.check(type)) {
619
+ this.advance();
620
+ return true;
621
+ }
622
+ }
623
+ return false;
624
+ }
625
+
626
+ private check(type: TokenType): boolean {
627
+ if (this.isAtEnd()) return false;
628
+ return this.peek().type === type;
629
+ }
630
+
631
+ private advance(): Token {
632
+ if (!this.isAtEnd()) this.current++;
633
+ return this.previous();
634
+ }
635
+
636
+ private isAtEnd(): boolean {
637
+ return this.peek().type === TokenType.EOF;
638
+ }
639
+
640
+ private peek(): Token {
641
+ return this.tokens[this.current]!;
642
+ }
643
+
644
+ private previous(): Token {
645
+ return this.tokens[this.current - 1]!;
646
+ }
647
+
648
+ private consume(type: TokenType, message: string): Token {
649
+ if (this.check(type)) return this.advance();
650
+ throw this.error(message);
651
+ }
652
+
653
+ private error(message: string): ParseError {
654
+ const pos = this.peek().position;
655
+ const fullMessage = `${message} at line ${pos.line}, column ${pos.column}`;
656
+ return new ParseError(fullMessage, pos, this.peek());
657
+ }
658
+
659
+ private isRightAssociative(op: Token): boolean {
660
+ // FHIRPath doesn't have right-associative operators
661
+ return false;
662
+ }
663
+
664
+ // Check if a token type is an operator keyword that can also be identifier/function
665
+ private isOperatorKeyword(type: TokenType): boolean {
666
+ return type === TokenType.DIV ||
667
+ type === TokenType.MOD ||
668
+ type === TokenType.CONTAINS ||
669
+ type === TokenType.IN ||
670
+ type === TokenType.AND ||
671
+ type === TokenType.OR ||
672
+ type === TokenType.XOR ||
673
+ type === TokenType.IMPLIES ||
674
+ type === TokenType.IS ||
675
+ type === TokenType.AS ||
676
+ type === TokenType.NOT ||
677
+ type === TokenType.TRUE ||
678
+ type === TokenType.FALSE;
679
+ }
680
+
681
+ // Synchronization points for error recovery
682
+ private synchronize() {
683
+ while (!this.isAtEnd()) {
684
+ if (this.previous().type === TokenType.COMMA) return;
685
+ if (this.previous().type === TokenType.RPAREN) return;
686
+
687
+ switch (this.peek().type) {
688
+ case TokenType.IDENTIFIER:
689
+ return;
690
+ }
691
+
692
+ this.advance();
693
+ }
694
+ }
695
+ }
696
+
697
+ // Export convenience function
698
+ export function parse(input: string | Token[]): ASTNode {
699
+ const parser = new FHIRPathParser(input);
700
+ return parser.parse();
701
+ }