@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.
- package/README.md +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8185 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +152 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
- package/src/runtime/context.ts +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
|
+
}
|