@atomic-ehr/fhirpath 0.0.1-canary.0c6931e.20250727185306
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +473 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +10307 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/analyzer/analyzer.ts +499 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +157 -0
- package/src/api/errors.ts +145 -0
- package/src/api/expression.ts +156 -0
- package/src/api/index.ts +122 -0
- package/src/api/inspect.ts +99 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +210 -0
- package/src/compiler/compiler.ts +546 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +24 -0
- package/src/index.ts +107 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +475 -0
- package/src/interpreter/types.ts +108 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/lexer2/index.md +232 -0
- package/src/lexer2/index.perf.test.ts +68 -0
- package/src/lexer2/index.test.ts +549 -0
- package/src/lexer2/index.ts +1251 -0
- package/src/lexer2/notes.md +173 -0
- package/src/lexer2/optimization-summary.md +718 -0
- package/src/parser/ast-factory.ts +220 -0
- package/src/parser/ast.ts +144 -0
- package/src/parser/collection-parser.ts +89 -0
- package/src/parser/diagnostic-messages.ts +216 -0
- package/src/parser/diagnostics.ts +85 -0
- package/src/parser/error-reporter.ts +230 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/literal-parser.ts +103 -0
- package/src/parser/parse-error.ts +16 -0
- package/src/parser/parser-error-factory.ts +141 -0
- package/src/parser/parser-state.ts +134 -0
- package/src/parser/parser.ts +1272 -0
- package/src/parser/pprint.ts +169 -0
- package/src/parser/precedence-manager.ts +64 -0
- package/src/parser/source-mapper.ts +248 -0
- package/src/parser/special-constructs.ts +142 -0
- package/src/parser/token-navigator.ts +110 -0
- package/src/parser/types.ts +60 -0
- package/src/parser2/index.md +177 -0
- package/src/parser2/index.perf.test.ts +184 -0
- package/src/parser2/index.test.ts +305 -0
- package/src/parser2/index.ts +578 -0
- package/src/parser2/optimization-summary.md +176 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +96 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +439 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/navigation.ts +52 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +308 -0
- package/src/registry/operations/utility.ts +644 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
- package/src/runtime/context.ts +158 -0
- package/src/runtime/debug-context.ts +135 -0
|
@@ -0,0 +1,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
|
+
}
|