@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,169 @@
1
+ import type {
2
+ ASTNode,
3
+ IdentifierNode,
4
+ TypeOrIdentifierNode,
5
+ VariableNode,
6
+ TypeReferenceNode,
7
+ BinaryNode,
8
+ UnaryNode,
9
+ UnionNode,
10
+ FunctionNode,
11
+ CollectionNode,
12
+ MembershipTestNode,
13
+ TypeCastNode,
14
+ IndexNode,
15
+ LiteralNode
16
+ } from './ast';
17
+ import { NodeType } from './ast';
18
+ import { TokenType } from '../lexer/token';
19
+
20
+ /**
21
+ * Pretty print an AST node as an S-expression (without position information)
22
+ */
23
+ export function pprint(node: ASTNode, multiline: boolean = false, indent: number = 0): string {
24
+ const spaces = multiline ? ' '.repeat(indent) : '';
25
+ const nl = multiline ? '\n' : '';
26
+ const childIndent = indent + 2;
27
+
28
+ switch (node.type) {
29
+ case NodeType.Literal:
30
+ const lit = node as LiteralNode;
31
+ return `${spaces}(${pprintLiteralValue(lit)}:${lit.valueType})`;
32
+
33
+ case NodeType.Identifier:
34
+ return `${spaces}(${(node as IdentifierNode).name}:id)`;
35
+
36
+ case NodeType.TypeOrIdentifier:
37
+ return `${spaces}(${(node as TypeOrIdentifierNode).name}:type-or-id)`;
38
+
39
+ case NodeType.Variable:
40
+ const varNode = node as VariableNode;
41
+ const varName = varNode.name.startsWith('$') ? varNode.name : `%${varNode.name}`;
42
+ return `${spaces}(${varName}:var)`;
43
+
44
+ case NodeType.TypeReference:
45
+ return `${spaces}(${(node as TypeReferenceNode).typeName}:type-ref)`;
46
+
47
+ case NodeType.Binary:
48
+ const binary = node as BinaryNode;
49
+ const op = tokenToOp(binary.operator);
50
+ if (multiline) {
51
+ return `${spaces}(${op}${nl}${pprint(binary.left, multiline, childIndent)}${nl}${pprint(binary.right, multiline, childIndent)})`;
52
+ } else {
53
+ return `(${op} ${pprint(binary.left)} ${pprint(binary.right)})`;
54
+ }
55
+
56
+ case NodeType.Unary:
57
+ const unary = node as UnaryNode;
58
+ const unaryOp = tokenToOp(unary.operator);
59
+ if (multiline) {
60
+ return `${spaces}(${unaryOp}:unary${nl}${pprint(unary.operand, multiline, childIndent)})`;
61
+ } else {
62
+ return `(${unaryOp}:unary ${pprint(unary.operand)})`;
63
+ }
64
+
65
+ case NodeType.Union:
66
+ const union = node as UnionNode;
67
+ if (multiline) {
68
+ const operands = union.operands.map(op => pprint(op, multiline, childIndent)).join(nl);
69
+ return `${spaces}(|${nl}${operands})`;
70
+ } else {
71
+ return `(| ${union.operands.map(op => pprint(op)).join(' ')})`;
72
+ }
73
+
74
+ case NodeType.Function:
75
+ const func = node as FunctionNode;
76
+ const funcName = (func.name as IdentifierNode).name;
77
+ if (multiline && func.arguments.length > 0) {
78
+ const args = func.arguments.map(arg => pprint(arg, multiline, childIndent)).join(nl);
79
+ return `${spaces}(${funcName}:fn${nl}${args})`;
80
+ } else {
81
+ const args = func.arguments.map(arg => pprint(arg)).join(' ');
82
+ return args ? `${spaces}(${funcName}:fn ${args})` : `${spaces}(${funcName}:fn)`;
83
+ }
84
+
85
+ case NodeType.Collection:
86
+ const coll = node as CollectionNode;
87
+ if (multiline && coll.elements.length > 0) {
88
+ const elements = coll.elements.map(el => pprint(el, multiline, childIndent)).join(nl);
89
+ return `${spaces}({}:collection${nl}${elements})`;
90
+ } else {
91
+ const elements = coll.elements.map(el => pprint(el)).join(' ');
92
+ return elements ? `${spaces}({}:collection ${elements})` : `${spaces}({}:collection)`;
93
+ }
94
+
95
+ case NodeType.MembershipTest:
96
+ const memberTest = node as MembershipTestNode;
97
+ if (multiline) {
98
+ return `${spaces}(is${nl}${pprint(memberTest.expression, multiline, childIndent)}${nl}${' '.repeat(childIndent)}${memberTest.targetType})`;
99
+ } else {
100
+ return `(is ${pprint(memberTest.expression)} ${memberTest.targetType})`;
101
+ }
102
+
103
+ case NodeType.TypeCast:
104
+ const typeCast = node as TypeCastNode;
105
+ if (multiline) {
106
+ return `${spaces}(as${nl}${pprint(typeCast.expression, multiline, childIndent)}${nl}${' '.repeat(childIndent)}${typeCast.targetType})`;
107
+ } else {
108
+ return `(as ${pprint(typeCast.expression)} ${typeCast.targetType})`;
109
+ }
110
+
111
+ case NodeType.Index:
112
+ const index = node as IndexNode;
113
+ if (multiline) {
114
+ return `${spaces}([]${nl}${pprint(index.expression, multiline, childIndent)}${nl}${pprint(index.index, multiline, childIndent)})`;
115
+ } else {
116
+ return `([] ${pprint(index.expression)} ${pprint(index.index)})`;
117
+ }
118
+
119
+ default:
120
+ return `${spaces}(unknown:${node.type})`;
121
+ }
122
+ }
123
+
124
+ function pprintLiteralValue(node: LiteralNode): string {
125
+ const value = node.value;
126
+ const valueType = node.valueType;
127
+
128
+ switch (valueType) {
129
+ case 'string':
130
+ return `'${value}'`;
131
+ case 'number':
132
+ return String(value);
133
+ case 'boolean':
134
+ return String(value);
135
+ case 'null':
136
+ return '{}';
137
+ case 'date':
138
+ case 'time':
139
+ case 'datetime':
140
+ return `@${value}`;
141
+ default:
142
+ return String(value);
143
+ }
144
+ }
145
+
146
+ function tokenToOp(token: TokenType): string {
147
+ switch (token) {
148
+ case TokenType.PLUS: return '+';
149
+ case TokenType.MINUS: return '-';
150
+ case TokenType.STAR: return '*';
151
+ case TokenType.DIV: return 'div';
152
+ case TokenType.MOD: return 'mod';
153
+ case TokenType.GT: return '>';
154
+ case TokenType.LT: return '<';
155
+ case TokenType.GTE: return '>=';
156
+ case TokenType.LTE: return '<=';
157
+ case TokenType.EQ: return '=';
158
+ case TokenType.NEQ: return '!=';
159
+ case TokenType.AND: return 'and';
160
+ case TokenType.OR: return 'or';
161
+ case TokenType.XOR: return 'xor';
162
+ case TokenType.IMPLIES: return 'implies';
163
+ case TokenType.NOT: return 'not';
164
+ case TokenType.DOT: return '.';
165
+ case TokenType.CONTAINS: return 'contains';
166
+ case TokenType.IN: return 'in';
167
+ default: return token;
168
+ }
169
+ }
@@ -0,0 +1,64 @@
1
+ import type { Token } from '../lexer/token';
2
+ import { TokenType } from '../lexer/token';
3
+ import { Registry } from '../registry';
4
+
5
+ /**
6
+ * Manages operator precedence for the parser
7
+ */
8
+ export class PrecedenceManager {
9
+ /**
10
+ * Get precedence for a token
11
+ * Higher number = higher precedence
12
+ */
13
+ static getPrecedence(token: Token): number {
14
+ // Special case for DOT which might not be in registry yet
15
+ if (token.type === TokenType.DOT) return 13;
16
+
17
+ // Use registry directly - both now use standard convention
18
+ return Registry.getPrecedence(token.type);
19
+ }
20
+
21
+ /**
22
+ * Check if an operator is right-associative
23
+ */
24
+ static isRightAssociative(op: Token): boolean {
25
+ // FHIRPath doesn't have right-associative operators
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * Get associativity adjustment for parsing
31
+ * For left-associative operators, we parse right side with precedence + 1
32
+ * For right-associative operators, we parse with same precedence
33
+ */
34
+ static getAssociativityAdjustment(op: Token): number {
35
+ return this.isRightAssociative(op) ? 0 : 1;
36
+ }
37
+
38
+ /**
39
+ * Check if precedence allows continuation of expression parsing
40
+ */
41
+ static shouldContinueParsing(tokenPrecedence: number, minPrecedence: number): boolean {
42
+ return tokenPrecedence !== 0 && tokenPrecedence >= minPrecedence;
43
+ }
44
+
45
+ /**
46
+ * Standard precedence levels for reference
47
+ */
48
+ static readonly PRECEDENCE_LEVELS = {
49
+ NONE: 0,
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
+ }
@@ -0,0 +1,248 @@
1
+ import type { Token } from '../lexer/token';
2
+ import type { ASTNode } from './ast';
3
+ import { NodeType } from './ast';
4
+ import type { TextRange, Position } from './types';
5
+
6
+ export class SourceMapper {
7
+ private source: string;
8
+ private lineOffsets: number[] = [];
9
+
10
+ constructor(source: string) {
11
+ this.source = source;
12
+ this.calculateLineOffsets();
13
+ }
14
+
15
+ private calculateLineOffsets(): void {
16
+ this.lineOffsets = [0];
17
+ for (let i = 0; i < this.source.length; i++) {
18
+ if (this.source[i] === '\n') {
19
+ this.lineOffsets.push(i + 1);
20
+ }
21
+ }
22
+ }
23
+
24
+ tokenToRange(token: Token): TextRange {
25
+ const start = this.offsetToPosition(token.position.offset);
26
+ const end = this.offsetToPosition(token.position.offset + token.value.length);
27
+
28
+ return {
29
+ start,
30
+ end
31
+ };
32
+ }
33
+
34
+ offsetToPosition(offset: number): Position {
35
+ let line = 0;
36
+
37
+ for (let i = 0; i < this.lineOffsets.length - 1; i++) {
38
+ if (offset >= this.lineOffsets[i]! && offset < this.lineOffsets[i + 1]!) {
39
+ line = i;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (offset >= this.lineOffsets[this.lineOffsets.length - 1]!) {
45
+ line = this.lineOffsets.length - 1;
46
+ }
47
+
48
+ const character = offset - this.lineOffsets[line]!;
49
+
50
+ return {
51
+ line,
52
+ character,
53
+ offset
54
+ };
55
+ }
56
+
57
+ positionToOffset(line: number, character: number): number {
58
+ if (line < 0 || line >= this.lineOffsets.length) {
59
+ return -1;
60
+ }
61
+
62
+ return this.lineOffsets[line]! + character;
63
+ }
64
+
65
+ nodeToRange(node: ASTNode): TextRange {
66
+ // If the node already has a range (from diagnostic mode), use it
67
+ if (node.range) {
68
+ return node.range;
69
+ }
70
+
71
+ const start = this.offsetToPosition(node.position.offset);
72
+ const end = this.calculateNodeEnd(node);
73
+
74
+ return {
75
+ start,
76
+ end
77
+ };
78
+ }
79
+
80
+ private calculateNodeEnd(node: ASTNode): Position {
81
+ let endOffset = node.position.offset;
82
+
83
+ switch (node.type) {
84
+ case NodeType.Literal: {
85
+ const literalNode = node as any;
86
+ if (literalNode.raw) {
87
+ endOffset += literalNode.raw.length;
88
+ } else if (typeof literalNode.value === 'string') {
89
+ endOffset += literalNode.value.length + 2; // +2 for quotes
90
+ } else if (typeof literalNode.value === 'number') {
91
+ endOffset += literalNode.value.toString().length;
92
+ } else if (typeof literalNode.value === 'boolean') {
93
+ endOffset += literalNode.value ? 4 : 5; // true/false
94
+ } else if (literalNode.value === null) {
95
+ endOffset += 4; // null
96
+ }
97
+ break;
98
+ }
99
+
100
+ case NodeType.Identifier:
101
+ case NodeType.TypeOrIdentifier: {
102
+ const identifierNode = node as any;
103
+ endOffset += identifierNode.name.length;
104
+ break;
105
+ }
106
+
107
+ case NodeType.Variable: {
108
+ const variableNode = node as any;
109
+ endOffset += variableNode.name.length + 1; // +1 for $ or %
110
+ break;
111
+ }
112
+
113
+ case NodeType.Binary: {
114
+ const binaryNode = node as any;
115
+ if (binaryNode.right && binaryNode.right.range) {
116
+ return binaryNode.right.range.end;
117
+ } else if (binaryNode.right) {
118
+ return this.calculateNodeEnd(binaryNode.right);
119
+ }
120
+ break;
121
+ }
122
+
123
+ case NodeType.Unary: {
124
+ const unaryNode = node as any;
125
+ if (unaryNode.operand && unaryNode.operand.range) {
126
+ return unaryNode.operand.range.end;
127
+ } else if (unaryNode.operand) {
128
+ return this.calculateNodeEnd(unaryNode.operand);
129
+ }
130
+ break;
131
+ }
132
+
133
+ case NodeType.Function: {
134
+ const functionNode = node as any;
135
+ // Estimate end as after the closing parenthesis
136
+ // This is approximate - proper implementation would track tokens
137
+ if (functionNode.arguments && functionNode.arguments.length > 0) {
138
+ const lastArg = functionNode.arguments[functionNode.arguments.length - 1];
139
+ if (lastArg.range) {
140
+ endOffset = lastArg.range.end.offset + 1; // +1 for )
141
+ } else {
142
+ endOffset = this.calculateNodeEnd(lastArg).offset + 1;
143
+ }
144
+ } else {
145
+ // Empty function call - estimate
146
+ endOffset += 2; // ()
147
+ }
148
+ break;
149
+ }
150
+
151
+ case NodeType.Index: {
152
+ const indexNode = node as any;
153
+ if (indexNode.index && indexNode.index.range) {
154
+ return this.offsetToPosition(indexNode.index.range.end.offset + 1); // +1 for ]
155
+ } else if (indexNode.index) {
156
+ const indexEnd = this.calculateNodeEnd(indexNode.index);
157
+ return this.offsetToPosition(indexEnd.offset + 1); // +1 for ]
158
+ }
159
+ break;
160
+ }
161
+
162
+ case NodeType.Collection: {
163
+ const collectionNode = node as any;
164
+ if (collectionNode.elements && collectionNode.elements.length > 0) {
165
+ const lastElement = collectionNode.elements[collectionNode.elements.length - 1];
166
+ if (lastElement.range) {
167
+ endOffset = lastElement.range.end.offset + 1; // +1 for }
168
+ } else {
169
+ endOffset = this.calculateNodeEnd(lastElement).offset + 1;
170
+ }
171
+ } else {
172
+ endOffset += 2; // {}
173
+ }
174
+ break;
175
+ }
176
+
177
+ case NodeType.Union: {
178
+ const unionNode = node as any;
179
+ if (unionNode.operands && unionNode.operands.length > 0) {
180
+ const lastOperand = unionNode.operands[unionNode.operands.length - 1];
181
+ if (lastOperand.range) {
182
+ return lastOperand.range.end;
183
+ } else {
184
+ return this.calculateNodeEnd(lastOperand);
185
+ }
186
+ }
187
+ break;
188
+ }
189
+
190
+ case NodeType.TypeCast:
191
+ case NodeType.MembershipTest: {
192
+ const typedNode = node as any;
193
+ if (typedNode.targetType) {
194
+ endOffset += typedNode.targetType.length;
195
+ }
196
+ break;
197
+ }
198
+
199
+ case NodeType.TypeReference: {
200
+ const typeRefNode = node as any;
201
+ endOffset += typeRefNode.typeName.length;
202
+ break;
203
+ }
204
+
205
+ case NodeType.Error:
206
+ case NodeType.Incomplete: {
207
+ // Error nodes should have their range set during creation
208
+ const errorNode = node as any;
209
+ if (errorNode.actualToken) {
210
+ endOffset += errorNode.actualToken.value.length;
211
+ } else {
212
+ endOffset += 1; // Minimal range
213
+ }
214
+ break;
215
+ }
216
+
217
+ default:
218
+ // For unknown nodes, minimal range
219
+ endOffset += 1;
220
+ }
221
+
222
+ return this.offsetToPosition(endOffset);
223
+ }
224
+
225
+ getRangeText(range: TextRange): string {
226
+ return this.source.substring(range.start.offset, range.end.offset);
227
+ }
228
+
229
+ getLineText(line: number): string {
230
+ if (line < 0 || line >= this.lineOffsets.length) {
231
+ return '';
232
+ }
233
+
234
+ const start = this.lineOffsets[line]!;
235
+ const end = line < this.lineOffsets.length - 1
236
+ ? this.lineOffsets[line + 1]! - 1
237
+ : this.source.length;
238
+
239
+ return this.source.substring(start, end).replace(/[\r\n]+$/, '');
240
+ }
241
+
242
+ mergeRanges(range1: TextRange, range2: TextRange): TextRange {
243
+ return {
244
+ start: range1.start.offset <= range2.start.offset ? range1.start : range2.start,
245
+ end: range1.end.offset >= range2.end.offset ? range1.end : range2.end
246
+ };
247
+ }
248
+ }
@@ -0,0 +1,142 @@
1
+ import type { Token } from '../lexer/token';
2
+ import { TokenType } from '../lexer/token';
3
+ import type { ASTNode, IdentifierNode, FunctionNode, TypeReferenceNode } from './ast';
4
+ import { NodeType } from './ast';
5
+ import { ASTFactory } from './ast-factory';
6
+ import type { TokenNavigator } from './token-navigator';
7
+ import { ErrorCode } from '../api/errors';
8
+
9
+ /**
10
+ * Handles parsing of special FHIRPath constructs
11
+ */
12
+ export class SpecialConstructs {
13
+ /**
14
+ * Parse special function syntax like ofType(Type)
15
+ */
16
+ static parseOfType(navigator: TokenNavigator, errorCallback: (msg: string) => Error): ASTNode {
17
+ const ofTypeToken = navigator.previous();
18
+
19
+ // Expect (
20
+ navigator.consume(TokenType.LPAREN, () =>
21
+ errorCallback("Expected '(' after 'ofType'")
22
+ );
23
+
24
+ // Expect type name
25
+ const typeToken = navigator.consume(TokenType.IDENTIFIER, () =>
26
+ errorCallback("Expected type name in ofType()")
27
+ );
28
+
29
+ // Expect )
30
+ navigator.consume(TokenType.RPAREN, () =>
31
+ errorCallback("Expected ')' after type name")
32
+ );
33
+
34
+ return ASTFactory.createFunction(
35
+ ASTFactory.createIdentifier('ofType', ofTypeToken.position),
36
+ [ASTFactory.createTypeReference(typeToken.value, typeToken.position)],
37
+ ofTypeToken.position
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Parse type operators (is/as) with their special syntax
43
+ */
44
+ static parseTypeOperator(
45
+ navigator: TokenNavigator,
46
+ left: ASTNode,
47
+ op: Token,
48
+ errorCallback: (msg: string, code?: ErrorCode) => Error
49
+ ): ASTNode {
50
+ // Type name can be either a simple identifier or in parentheses (for is() function syntax)
51
+ let typeName: string;
52
+
53
+ if (navigator.check(TokenType.LPAREN)) {
54
+ // Handle is(TypeName) syntax
55
+ navigator.advance(); // consume (
56
+ const typeToken = navigator.consume(TokenType.IDENTIFIER, () =>
57
+ errorCallback("Expected type name", ErrorCode.EXPECTED_IDENTIFIER)
58
+ );
59
+ typeName = typeToken.value;
60
+ navigator.consume(TokenType.RPAREN, () =>
61
+ errorCallback("Expected ')' after type name", ErrorCode.UNCLOSED_PARENTHESIS)
62
+ );
63
+ } else {
64
+ // Regular is TypeName syntax
65
+ const typeToken = navigator.consume(TokenType.IDENTIFIER, () =>
66
+ errorCallback(`Expected type name after '${op.value}'`, ErrorCode.EXPECTED_IDENTIFIER)
67
+ );
68
+ typeName = typeToken.value;
69
+ }
70
+
71
+ if (op.type === TokenType.IS) {
72
+ return ASTFactory.createMembershipTest(left, typeName, op.position);
73
+ } else {
74
+ return ASTFactory.createTypeCast(left, typeName, op.position);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Parse union operator which can chain multiple operands
80
+ */
81
+ static parseUnion(left: ASTNode, precedence: number, parseExpression: (p: number) => ASTNode): ASTNode {
82
+ const right = parseExpression(precedence + 1);
83
+
84
+ // If left is already a union, add to it
85
+ if (left.type === NodeType.Union) {
86
+ (left as any).operands.push(right);
87
+ return left;
88
+ }
89
+
90
+ // Create new union node
91
+ return ASTFactory.createUnion([left, right], (left as any).position);
92
+ }
93
+
94
+ /**
95
+ * Check for common mistakes like == instead of =
96
+ */
97
+ static checkDoubleEquals(
98
+ navigator: TokenNavigator,
99
+ op: Token,
100
+ diagnosticsCallback?: (start: Token, end: Token, message: string, code: ErrorCode) => void
101
+ ): void {
102
+ if (op.type === TokenType.EQ && navigator.check(TokenType.EQ)) {
103
+ const secondEq = navigator.peek();
104
+ if (diagnosticsCallback) {
105
+ diagnosticsCallback(
106
+ op,
107
+ secondEq,
108
+ "'==' is not valid in FHIRPath, use '=' for equality",
109
+ ErrorCode.INVALID_OPERATOR
110
+ );
111
+ // Skip the extra = to avoid cascading errors
112
+ navigator.advance();
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Check for double dot error
119
+ */
120
+ static checkDoubleDot(
121
+ navigator: TokenNavigator,
122
+ firstDot: Token,
123
+ markPartial: () => void,
124
+ diagnosticsCallback?: (start: Token, end: Token, message: string, code: ErrorCode) => void
125
+ ): void {
126
+ if (navigator.check(TokenType.DOT)) {
127
+ const secondDot = navigator.peek();
128
+ if (diagnosticsCallback) {
129
+ diagnosticsCallback(
130
+ firstDot,
131
+ secondDot,
132
+ "Invalid '..' operator - use a single '.' for navigation",
133
+ ErrorCode.INVALID_OPERATOR
134
+ );
135
+ }
136
+
137
+ // Skip the extra dot to avoid cascading errors
138
+ navigator.advance();
139
+ markPartial();
140
+ }
141
+ }
142
+ }