@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900
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 +716 -238
- package/dist/index.d.ts +226 -120
- package/dist/index.js +11552 -5580
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +939 -1204
- package/src/completion-provider.ts +209 -191
- package/src/complex-types/quantity-value.ts +410 -0
- package/src/complex-types/temporal.ts +1776 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +506 -468
- package/src/lexer.ts +192 -211
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +99 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +744 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +132 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/extension-function.ts +84 -0
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +7 -9
- package/src/operations/greater-or-equal-operator.ts +7 -9
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +193 -8
- package/src/operations/implies-operator.ts +2 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +43 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +70 -0
- package/src/operations/is-operator.ts +176 -13
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +8 -9
- package/src/operations/less-or-equal-operator.ts +7 -9
- package/src/operations/less-than.ts +8 -13
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +76 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +8 -2
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +10 -3
- package/src/operations/ofType-function.ts +43 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +125 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +78 -15
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +262 -503
- package/src/registry.ts +53 -42
- package/src/types.ts +129 -17
- package/src/utils/decimal.ts +76 -0
- package/src/utils/pprint.ts +151 -0
- package/src/quantity-value.ts +0 -198
package/src/parser.ts
CHANGED
|
@@ -2,23 +2,21 @@ import { Lexer, TokenType, Channel } from './lexer';
|
|
|
2
2
|
import type { Token, LexerOptions } from './lexer';
|
|
3
3
|
import { registry } from './registry';
|
|
4
4
|
import { NodeType } from './types';
|
|
5
|
-
import type { AnyCursorNode } from './cursor-nodes';
|
|
6
5
|
import {
|
|
7
|
-
CursorContext,
|
|
8
6
|
createCursorOperatorNode,
|
|
9
7
|
createCursorIdentifierNode,
|
|
10
8
|
createCursorArgumentNode,
|
|
11
9
|
createCursorIndexNode,
|
|
12
10
|
createCursorTypeNode,
|
|
13
|
-
} from './cursor-nodes';
|
|
11
|
+
} from './parser/cursor-nodes';
|
|
14
12
|
import type {
|
|
15
13
|
Position,
|
|
16
14
|
Range,
|
|
17
15
|
BaseASTNode,
|
|
18
16
|
ASTNode,
|
|
19
17
|
IdentifierNode,
|
|
20
|
-
TypeOrIdentifierNode,
|
|
21
18
|
LiteralNode,
|
|
19
|
+
TemporalLiteralNode,
|
|
22
20
|
BinaryNode,
|
|
23
21
|
UnaryNode,
|
|
24
22
|
FunctionNode,
|
|
@@ -34,7 +32,11 @@ import type {
|
|
|
34
32
|
ParseResult,
|
|
35
33
|
ParseError
|
|
36
34
|
} from './types';
|
|
37
|
-
import { Errors
|
|
35
|
+
import { Errors } from './errors';
|
|
36
|
+
import { parseTemporalLiteral } from './complex-types/temporal';
|
|
37
|
+
import { augment } from './analyzer/augmentor';
|
|
38
|
+
import { findNodeAtPosition, getCompletions as lspGetCompletions, getExpectedTokens as lspGetExpectedTokens } from './analyzer/cursor-services';
|
|
39
|
+
import { computeTriviaSpans } from './analyzer/trivia-indexer';
|
|
38
40
|
|
|
39
41
|
// Re-export types for backward compatibility
|
|
40
42
|
export {
|
|
@@ -44,8 +46,8 @@ export {
|
|
|
44
46
|
type Range,
|
|
45
47
|
type ASTNode,
|
|
46
48
|
type IdentifierNode,
|
|
47
|
-
type TypeOrIdentifierNode,
|
|
48
49
|
type LiteralNode,
|
|
50
|
+
type TemporalLiteralNode,
|
|
49
51
|
type BinaryNode,
|
|
50
52
|
type UnaryNode,
|
|
51
53
|
type FunctionNode,
|
|
@@ -61,6 +63,7 @@ export {
|
|
|
61
63
|
type ParseResult,
|
|
62
64
|
type ParseError
|
|
63
65
|
} from './types';
|
|
66
|
+
export { pprint } from './utils/pprint';
|
|
64
67
|
|
|
65
68
|
// Parser options
|
|
66
69
|
export interface ParserOptions {
|
|
@@ -80,13 +83,14 @@ export class Parser {
|
|
|
80
83
|
protected current = 0;
|
|
81
84
|
private mode: 'simple' | 'lsp';
|
|
82
85
|
private options: ParserOptions;
|
|
86
|
+
private preserveTriviaEffective = false;
|
|
83
87
|
private errors?: ParseError[];
|
|
84
|
-
private nodeIdCounter?: number;
|
|
85
|
-
private nodeIndex?: Map<string, ASTNode>;
|
|
86
|
-
private nodesByType?: Map<NodeType | 'Error', ASTNode[]>;
|
|
87
|
-
private identifierIndex?: Map<string, ASTNode[]>;
|
|
88
|
-
private currentParent?: ASTNode | null;
|
|
89
88
|
private input: string;
|
|
89
|
+
// Trivia and token indexes for LSP mode with trivia preservation
|
|
90
|
+
private leadingTriviaByTokenStart?: Map<number, TriviaInfo[]>;
|
|
91
|
+
private trailingTriviaByTokenEnd?: Map<number, TriviaInfo[]>;
|
|
92
|
+
private tokenByStart?: Map<number, Token>;
|
|
93
|
+
private tokenByEnd?: Map<number, Token>;
|
|
90
94
|
|
|
91
95
|
// Synchronization tokens for error recovery
|
|
92
96
|
private readonly synchronizationTokens = new Set([
|
|
@@ -103,73 +107,75 @@ export class Parser {
|
|
|
103
107
|
trackPosition: true,
|
|
104
108
|
preserveTrivia: mode === 'lsp' ? true : (options.preserveTrivia ?? false)
|
|
105
109
|
};
|
|
110
|
+
this.preserveTriviaEffective = !!lexerOptions.preserveTrivia;
|
|
106
111
|
|
|
107
112
|
this.lexer = new Lexer(input, lexerOptions);
|
|
108
113
|
this.tokens = this.lexer.tokenize();
|
|
109
114
|
|
|
110
|
-
//
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
// If preserving trivia, capture leading/trailing trivia spans before filtering
|
|
116
|
+
if (this.preserveTriviaEffective) {
|
|
117
|
+
const spans = computeTriviaSpans(this.tokens);
|
|
118
|
+
this.leadingTriviaByTokenStart = spans.leadingByStart;
|
|
119
|
+
this.trailingTriviaByTokenEnd = spans.trailingByEnd;
|
|
120
|
+
this.tokenByStart = spans.tokenByStart;
|
|
121
|
+
this.tokenByEnd = spans.tokenByEnd;
|
|
122
|
+
// Then filter hidden-channel tokens out for parsing
|
|
123
|
+
this.tokens = this.tokens.filter(token => token.channel === undefined || token.channel === Channel.DEFAULT);
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
// Make mode/options/input available before cursor injection decisions
|
|
127
|
+
this.input = input;
|
|
128
|
+
this.mode = mode;
|
|
129
|
+
this.options = options;
|
|
130
|
+
|
|
117
131
|
// Inject cursor token if cursor position is provided
|
|
118
132
|
if (options.cursorPosition !== undefined) {
|
|
119
133
|
this.tokens = this.injectCursorToken(this.tokens, options.cursorPosition);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
this.input = input;
|
|
123
|
-
this.mode = mode;
|
|
124
|
-
this.options = options;
|
|
125
|
-
|
|
126
136
|
// Initialize LSP features only if needed
|
|
127
137
|
if (this.mode === 'lsp') {
|
|
128
138
|
this.errors = [];
|
|
129
|
-
|
|
130
|
-
this.nodeIndex = new Map();
|
|
131
|
-
this.nodesByType = new Map();
|
|
132
|
-
this.identifierIndex = new Map();
|
|
133
|
-
this.currentParent = null;
|
|
139
|
+
// indexes are now built by the augmentor
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (this.peek().type === TokenType.CURSOR) {
|
|
139
|
-
return null; // Will be handled contextually
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
142
|
+
|
|
143
|
+
// removed unused checkCursor(); cursor handling is contextual in parse methods
|
|
143
144
|
|
|
144
145
|
private injectCursorToken(tokens: Token[], cursorPosition: number): Token[] {
|
|
145
146
|
// Find the position to inject the cursor token
|
|
146
147
|
let insertIndex = 0;
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
for (let i = 0; i < tokens.length; i++) {
|
|
149
150
|
const token = tokens[i];
|
|
150
151
|
if (!token) continue;
|
|
151
|
-
|
|
152
|
+
|
|
152
153
|
// Skip EOF token
|
|
153
154
|
if (token.type === TokenType.EOF) {
|
|
154
155
|
break;
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
// Check if cursor is before this token
|
|
158
159
|
if (cursorPosition <= token.start) {
|
|
159
160
|
insertIndex = i;
|
|
160
161
|
break;
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
+
|
|
163
164
|
// Check if cursor is within this token (we ignore mid-token cursors)
|
|
164
165
|
if (cursorPosition > token.start && cursorPosition < token.end) {
|
|
165
|
-
//
|
|
166
|
-
|
|
166
|
+
// Only materialize mid-token cursor in LSP mode; otherwise ignore
|
|
167
|
+
if (this.mode === 'lsp') {
|
|
168
|
+
insertIndex = i;
|
|
169
|
+
break;
|
|
170
|
+
} else {
|
|
171
|
+
return tokens;
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
|
|
169
175
|
// Cursor is after this token
|
|
170
176
|
insertIndex = i + 1;
|
|
171
177
|
}
|
|
172
|
-
|
|
178
|
+
|
|
173
179
|
// Create cursor token
|
|
174
180
|
const cursorToken: Token = {
|
|
175
181
|
type: TokenType.CURSOR,
|
|
@@ -183,14 +189,14 @@ export class Parser {
|
|
|
183
189
|
end: { line: 0, character: cursorPosition, offset: cursorPosition }
|
|
184
190
|
}
|
|
185
191
|
};
|
|
186
|
-
|
|
192
|
+
|
|
187
193
|
// Insert cursor token
|
|
188
194
|
const result = [...tokens];
|
|
189
195
|
result.splice(insertIndex, 0, cursorToken);
|
|
190
|
-
|
|
196
|
+
|
|
191
197
|
return result;
|
|
192
198
|
}
|
|
193
|
-
|
|
199
|
+
|
|
194
200
|
private getRangeFromToken(token: Token): Range {
|
|
195
201
|
return token.range || {
|
|
196
202
|
start: { line: 0, character: 0, offset: token.start },
|
|
@@ -257,9 +263,7 @@ export class Parser {
|
|
|
257
263
|
private parseLSP(): ParseResult {
|
|
258
264
|
// Clear indexes for fresh parse
|
|
259
265
|
this.errors = [];
|
|
260
|
-
|
|
261
|
-
this.nodesByType!.clear();
|
|
262
|
-
this.identifierIndex!.clear();
|
|
266
|
+
// indexes will be built by augmentor
|
|
263
267
|
|
|
264
268
|
let ast: ASTNode;
|
|
265
269
|
|
|
@@ -270,6 +274,8 @@ export class Parser {
|
|
|
270
274
|
const token = this.peek();
|
|
271
275
|
this.addError(Errors.unexpectedToken(token.value || TokenType[token.type], this.getRangeFromToken(token)).message, token);
|
|
272
276
|
}
|
|
277
|
+
|
|
278
|
+
// No transform here; augmentor handles cursor-specific transforms
|
|
273
279
|
} catch (error) {
|
|
274
280
|
// In LSP mode, create error node on fatal errors
|
|
275
281
|
if (error instanceof Error) {
|
|
@@ -279,28 +285,37 @@ export class Parser {
|
|
|
279
285
|
}
|
|
280
286
|
}
|
|
281
287
|
|
|
288
|
+
// Augment AST for LSP consumers
|
|
289
|
+
const aug = augment(ast, {
|
|
290
|
+
input: this.input,
|
|
291
|
+
preserveTrivia: this.preserveTriviaEffective,
|
|
292
|
+
trivia: this.preserveTriviaEffective ? {
|
|
293
|
+
leadingByStart: this.leadingTriviaByTokenStart,
|
|
294
|
+
trailingByEnd: this.trailingTriviaByTokenEnd,
|
|
295
|
+
} : undefined,
|
|
296
|
+
cursorPosition: this.options.cursorPosition,
|
|
297
|
+
});
|
|
298
|
+
|
|
282
299
|
const result: ParseResult = {
|
|
283
|
-
ast,
|
|
300
|
+
ast: aug.ast,
|
|
284
301
|
errors: this.errors!,
|
|
285
|
-
indexes:
|
|
286
|
-
nodeById: this.nodeIndex!,
|
|
287
|
-
nodesByType: this.nodesByType!,
|
|
288
|
-
identifiers: this.identifierIndex!
|
|
289
|
-
}
|
|
302
|
+
indexes: aug.indexes,
|
|
290
303
|
};
|
|
291
304
|
|
|
292
305
|
// Add cursor context if partial parsing
|
|
293
306
|
if (this.options.partialParse) {
|
|
294
|
-
const nodeAtCursor =
|
|
307
|
+
const nodeAtCursor = findNodeAtPosition(aug.ast, this.options.partialParse.cursorPosition);
|
|
295
308
|
result.cursorContext = {
|
|
296
309
|
node: nodeAtCursor,
|
|
297
|
-
expectedTokens:
|
|
298
|
-
availableCompletions:
|
|
310
|
+
expectedTokens: lspGetExpectedTokens(nodeAtCursor),
|
|
311
|
+
availableCompletions: lspGetCompletions(nodeAtCursor, aug.indexes.identifiers)
|
|
299
312
|
};
|
|
300
313
|
}
|
|
301
314
|
|
|
302
315
|
return result;
|
|
303
316
|
}
|
|
317
|
+
|
|
318
|
+
// Trivia population moved to augmentor
|
|
304
319
|
|
|
305
320
|
// Shared expression parsing with precedence climbing
|
|
306
321
|
protected expression(): ASTNode {
|
|
@@ -329,8 +344,15 @@ export class Parser {
|
|
|
329
344
|
this.current++; // inline advance()
|
|
330
345
|
// Check for cursor in indexer
|
|
331
346
|
if (this.peek().type === TokenType.CURSOR) {
|
|
332
|
-
this.
|
|
333
|
-
|
|
347
|
+
if (this.mode === 'lsp') {
|
|
348
|
+
const cursorTok = this.advance();
|
|
349
|
+
const cursorIndex = createCursorIndexNode(cursorTok.start) as any;
|
|
350
|
+
this.consume(TokenType.RBRACKET, "Expected ']'");
|
|
351
|
+
left = this.createIndexNode(left, cursorIndex, token);
|
|
352
|
+
} else {
|
|
353
|
+
this.advance();
|
|
354
|
+
left = createCursorIndexNode(this.previous().start) as any;
|
|
355
|
+
}
|
|
334
356
|
} else {
|
|
335
357
|
const index = this.expression();
|
|
336
358
|
this.consume(TokenType.RBRACKET, "Expected ']'");
|
|
@@ -340,13 +362,8 @@ export class Parser {
|
|
|
340
362
|
}
|
|
341
363
|
|
|
342
364
|
if (token.type === TokenType.LPAREN && this.isFunctionCall(left)) {
|
|
343
|
-
// Function calls - always bind tightly
|
|
344
|
-
this.
|
|
345
|
-
const args = this.parseArgumentList();
|
|
346
|
-
this.consume(TokenType.RPAREN, "Expected ')'");
|
|
347
|
-
// For function calls, we need to find the start token from the name node
|
|
348
|
-
const startToken = this.tokens[this.current - args.length - 2] || token;
|
|
349
|
-
left = this.createFunctionNode(left, args, startToken);
|
|
365
|
+
// Function calls - always bind tightly, handled via shared helper
|
|
366
|
+
left = this.parseFunctionCall(left);
|
|
350
367
|
continue;
|
|
351
368
|
}
|
|
352
369
|
|
|
@@ -419,6 +436,18 @@ export class Parser {
|
|
|
419
436
|
return createCursorOperatorNode(token.start) as any;
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
// Quantity literal emitted by lexer
|
|
440
|
+
if (token.type === TokenType.QUANTITY) {
|
|
441
|
+
this.current++;
|
|
442
|
+
const raw = token.value;
|
|
443
|
+
const quoteIdx = raw.indexOf("'");
|
|
444
|
+
const numberPart = quoteIdx >= 0 ? raw.slice(0, quoteIdx).trim() : raw;
|
|
445
|
+
const unitPart = quoteIdx >= 0 ? raw.slice(quoteIdx) : '';
|
|
446
|
+
const numberValue = Number(numberPart);
|
|
447
|
+
const unit = unitPart ? this.parseStringValue(unitPart) : '';
|
|
448
|
+
return this.createQuantityNode(numberValue, unit, token, token);
|
|
449
|
+
}
|
|
450
|
+
|
|
422
451
|
if (token.type === TokenType.NUMBER) {
|
|
423
452
|
this.current++; // inline advance()
|
|
424
453
|
const numberValue = parseFloat(token.value);
|
|
@@ -428,7 +457,7 @@ export class Parser {
|
|
|
428
457
|
if (nextToken.type === TokenType.STRING) {
|
|
429
458
|
this.advance();
|
|
430
459
|
const unit = this.parseStringValue(nextToken.value);
|
|
431
|
-
return this.createQuantityNode(numberValue, unit,
|
|
460
|
+
return this.createQuantityNode(numberValue, unit, token, nextToken);
|
|
432
461
|
}
|
|
433
462
|
|
|
434
463
|
// Check if next token is a calendar duration identifier
|
|
@@ -438,11 +467,14 @@ export class Parser {
|
|
|
438
467
|
'second', 'seconds', 'millisecond', 'milliseconds'];
|
|
439
468
|
if (calendarUnits.includes(nextToken.value)) {
|
|
440
469
|
this.advance();
|
|
441
|
-
return this.createQuantityNode(numberValue, nextToken.value,
|
|
470
|
+
return this.createQuantityNode(numberValue, nextToken.value, token, nextToken);
|
|
442
471
|
}
|
|
443
472
|
}
|
|
444
473
|
|
|
445
|
-
|
|
474
|
+
// Determine if this is an integer or decimal based on the original token text
|
|
475
|
+
// If the token contains a decimal point, it's a decimal even if the value is a whole number
|
|
476
|
+
const isDecimal = token.value.includes('.');
|
|
477
|
+
return this.createLiteralNode(numberValue, isDecimal ? 'decimal' : 'number', token);
|
|
446
478
|
}
|
|
447
479
|
|
|
448
480
|
if (token.type === TokenType.STRING) {
|
|
@@ -461,16 +493,22 @@ export class Parser {
|
|
|
461
493
|
return this.createLiteralNode(null, 'null', token);
|
|
462
494
|
}
|
|
463
495
|
|
|
496
|
+
if (token.type === TokenType.DATE) {
|
|
497
|
+
this.advance();
|
|
498
|
+
const value = token.value.substring(1); // Remove @
|
|
499
|
+
return this.createTemporalLiteralNode(value, 'date', token);
|
|
500
|
+
}
|
|
501
|
+
|
|
464
502
|
if (token.type === TokenType.DATETIME) {
|
|
465
503
|
this.advance();
|
|
466
504
|
const value = token.value.substring(1); // Remove @
|
|
467
|
-
return this.
|
|
505
|
+
return this.createTemporalLiteralNode(value, 'datetime', token);
|
|
468
506
|
}
|
|
469
507
|
|
|
470
508
|
if (token.type === TokenType.TIME) {
|
|
471
509
|
this.advance();
|
|
472
510
|
const value = token.value.substring(1); // Remove @
|
|
473
|
-
return this.
|
|
511
|
+
return this.createTemporalLiteralNode(value, 'time', token);
|
|
474
512
|
}
|
|
475
513
|
|
|
476
514
|
if (token.type === TokenType.SPECIAL_IDENTIFIER) {
|
|
@@ -490,6 +528,15 @@ export class Parser {
|
|
|
490
528
|
}
|
|
491
529
|
|
|
492
530
|
if (token.type === TokenType.IDENTIFIER) {
|
|
531
|
+
// If cursor is exactly at end of identifier and no whitespace at cursor, treat as identifier context
|
|
532
|
+
if (this.options.cursorPosition !== undefined && this.options.cursorPosition === token.end) {
|
|
533
|
+
const ch = this.input[this.options.cursorPosition];
|
|
534
|
+
const isWs = ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === undefined;
|
|
535
|
+
if (!isWs) {
|
|
536
|
+
this.advance();
|
|
537
|
+
return createCursorIdentifierNode(this.options.cursorPosition, token.value) as any;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
493
540
|
this.advance();
|
|
494
541
|
const name = this.parseIdentifierValue(token.value);
|
|
495
542
|
return this.createIdentifierNode(name, token);
|
|
@@ -519,7 +566,7 @@ export class Parser {
|
|
|
519
566
|
const tokenStr = token.value || TokenType[token.type];
|
|
520
567
|
const range = this.getRangeFromToken(token);
|
|
521
568
|
const error = Errors.unexpectedToken(tokenStr, range);
|
|
522
|
-
return this.
|
|
569
|
+
return this.handleAstError(error.message, token);
|
|
523
570
|
}
|
|
524
571
|
|
|
525
572
|
protected parseInvocation(): ASTNode {
|
|
@@ -533,17 +580,19 @@ export class Parser {
|
|
|
533
580
|
|
|
534
581
|
// Allow identifiers and keywords that can be used as member names
|
|
535
582
|
if (token.type === TokenType.IDENTIFIER) {
|
|
583
|
+
// Check for cursor at the end of an identifier
|
|
584
|
+
if (this.options.cursorPosition !== undefined && this.options.cursorPosition === token.end) {
|
|
585
|
+
this.advance();
|
|
586
|
+
return createCursorIdentifierNode(this.options.cursorPosition, token.value) as any;
|
|
587
|
+
}
|
|
588
|
+
|
|
536
589
|
this.advance();
|
|
537
590
|
const name = this.parseIdentifierValue(token.value);
|
|
538
591
|
const node = this.createIdentifierNode(name, token);
|
|
539
592
|
|
|
540
593
|
// Check if this is a function call
|
|
541
594
|
if (this.check(TokenType.LPAREN)) {
|
|
542
|
-
this.
|
|
543
|
-
const args = this.parseArgumentList();
|
|
544
|
-
this.consume(TokenType.RPAREN, "Expected ')'");
|
|
545
|
-
const startToken = this.tokens[this.current - args.length - 2] || this.previous();
|
|
546
|
-
return this.createFunctionNode(node, args, startToken);
|
|
595
|
+
return this.parseFunctionCall(node);
|
|
547
596
|
}
|
|
548
597
|
|
|
549
598
|
return node;
|
|
@@ -558,17 +607,17 @@ export class Parser {
|
|
|
558
607
|
const tokenStr = token.value || TokenType[token.type];
|
|
559
608
|
const range = this.getRangeFromToken(token);
|
|
560
609
|
const error = Errors.expectedIdentifier('.', tokenStr, range);
|
|
561
|
-
return this.
|
|
610
|
+
return this.handleAstError(error.message, token);
|
|
562
611
|
}
|
|
563
612
|
|
|
564
|
-
protected parseArgumentList(): ASTNode[] {
|
|
613
|
+
protected parseArgumentList(functionName?: string): ASTNode[] {
|
|
565
614
|
const args: ASTNode[] = [];
|
|
566
615
|
|
|
567
616
|
// Check for cursor at start of arguments
|
|
568
617
|
if (this.peek().type === TokenType.CURSOR) {
|
|
569
618
|
this.advance();
|
|
570
|
-
|
|
571
|
-
args.push(createCursorArgumentNode(this.previous().start,
|
|
619
|
+
const fn = functionName ?? '';
|
|
620
|
+
args.push(createCursorArgumentNode(this.previous().start, fn, 0) as any);
|
|
572
621
|
return args;
|
|
573
622
|
}
|
|
574
623
|
|
|
@@ -582,7 +631,8 @@ export class Parser {
|
|
|
582
631
|
// Check for cursor after comma
|
|
583
632
|
if (this.peek().type === TokenType.CURSOR) {
|
|
584
633
|
this.advance();
|
|
585
|
-
|
|
634
|
+
const fn = functionName ?? '';
|
|
635
|
+
args.push(createCursorArgumentNode(this.previous().start, fn, args.length) as any);
|
|
586
636
|
return args;
|
|
587
637
|
}
|
|
588
638
|
args.push(this.expression());
|
|
@@ -591,20 +641,29 @@ export class Parser {
|
|
|
591
641
|
return args;
|
|
592
642
|
}
|
|
593
643
|
|
|
644
|
+
// Shared function-call parser for both standalone and dotted calls
|
|
645
|
+
protected parseFunctionCall(nameNode: ASTNode): ASTNode {
|
|
646
|
+
// Current token is '(' per caller contract
|
|
647
|
+
this.advance();
|
|
648
|
+
const fnName = (nameNode as any)?.name && (typeof (nameNode as any).name === 'string')
|
|
649
|
+
? (nameNode as any).name as string
|
|
650
|
+
: undefined;
|
|
651
|
+
const args = this.parseArgumentList(fnName);
|
|
652
|
+
this.consume(TokenType.RPAREN, "Expected ')'");
|
|
653
|
+
return this.createFunctionNode(nameNode, args);
|
|
654
|
+
}
|
|
655
|
+
|
|
594
656
|
protected parseCollectionElements(): ASTNode[] {
|
|
595
657
|
const elements: ASTNode[] = [];
|
|
596
658
|
|
|
597
659
|
if (this.peek().type === TokenType.RBRACE) {
|
|
598
|
-
return elements;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
elements.push(this.expression());
|
|
602
|
-
|
|
603
|
-
while (this.match(TokenType.COMMA)) {
|
|
604
|
-
elements.push(this.expression());
|
|
660
|
+
return elements; // Empty collection {} is valid
|
|
605
661
|
}
|
|
606
662
|
|
|
607
|
-
|
|
663
|
+
// Any other token is an error - braces can only contain empty collections
|
|
664
|
+
const unexpectedToken = this.peek();
|
|
665
|
+
const message = `Unexpected token '${unexpectedToken.value}', expected '}'. Braces can only be used for empty collections. Use parentheses and pipe operators for non-empty collections: (1 | 2 | 3)`;
|
|
666
|
+
return this.handleAstError(message, unexpectedToken) as any;
|
|
608
667
|
}
|
|
609
668
|
|
|
610
669
|
protected parseTypeName(): string {
|
|
@@ -613,8 +672,12 @@ export class Parser {
|
|
|
613
672
|
const tokenStr = token.value || TokenType[token.type];
|
|
614
673
|
const range = this.getRangeFromToken(token);
|
|
615
674
|
const error = Errors.expectedTypeName(tokenStr, range);
|
|
616
|
-
this.
|
|
617
|
-
|
|
675
|
+
if (this.mode === 'lsp' && this.options.errorRecovery) {
|
|
676
|
+
this.reportError(error.message, token);
|
|
677
|
+
return '';
|
|
678
|
+
}
|
|
679
|
+
this.throwSyntax(error.message, token);
|
|
680
|
+
return '';
|
|
618
681
|
}
|
|
619
682
|
return this.parseIdentifierValue(token.value);
|
|
620
683
|
}
|
|
@@ -622,7 +685,14 @@ export class Parser {
|
|
|
622
685
|
protected parseStringValue(raw: string): string {
|
|
623
686
|
// Remove quotes and handle escape sequences
|
|
624
687
|
const content = raw.slice(1, -1);
|
|
625
|
-
|
|
688
|
+
|
|
689
|
+
// Handle unicode escapes first (\uXXXX)
|
|
690
|
+
let result = content.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
|
|
691
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Then handle other escape sequences
|
|
695
|
+
result = result.replace(/\\(.)/g, (_, char) => {
|
|
626
696
|
switch (char) {
|
|
627
697
|
case 'n': return '\n';
|
|
628
698
|
case 'r': return '\r';
|
|
@@ -636,6 +706,8 @@ export class Parser {
|
|
|
636
706
|
default: return char;
|
|
637
707
|
}
|
|
638
708
|
});
|
|
709
|
+
|
|
710
|
+
return result;
|
|
639
711
|
}
|
|
640
712
|
|
|
641
713
|
protected parseIdentifierValue(raw: string): string {
|
|
@@ -648,36 +720,10 @@ export class Parser {
|
|
|
648
720
|
|
|
649
721
|
// Shared utility methods
|
|
650
722
|
protected isFunctionCall(node: ASTNode): boolean {
|
|
651
|
-
return (node as any).type === NodeType.Identifier
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Helper method to check if a token is a binary operator
|
|
655
|
-
protected isBinaryOperatorToken(token: Token): boolean {
|
|
656
|
-
if (token.type === TokenType.OPERATOR || token.type === TokenType.DOT) {
|
|
657
|
-
return registry.isBinaryOperator(token.value);
|
|
658
|
-
}
|
|
659
|
-
if (token.type === TokenType.IDENTIFIER) {
|
|
660
|
-
return registry.isKeywordOperator(token.value);
|
|
661
|
-
}
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
protected isKeywordAllowedAsMember(token: Token): boolean {
|
|
666
|
-
// Keywords that can be used as member names
|
|
667
|
-
if (token.type !== TokenType.IDENTIFIER) return false;
|
|
668
|
-
|
|
669
|
-
const keywordsAllowed = [
|
|
670
|
-
'contains', 'and', 'or', 'xor', 'implies',
|
|
671
|
-
'as', 'is', 'div', 'mod', 'in', 'true', 'false'
|
|
672
|
-
];
|
|
673
|
-
|
|
674
|
-
return keywordsAllowed.includes(token.value.toLowerCase());
|
|
723
|
+
return (node as any).type === NodeType.Identifier;
|
|
675
724
|
}
|
|
676
725
|
|
|
677
|
-
|
|
678
|
-
// Keywords that can be used as identifiers in certain contexts
|
|
679
|
-
return this.isKeywordAllowedAsMember(token);
|
|
680
|
-
}
|
|
726
|
+
// removed unused: isBinaryOperatorToken, isKeywordAllowedAsMember, isKeywordAllowedAsIdentifier
|
|
681
727
|
|
|
682
728
|
// Helper methods
|
|
683
729
|
protected peek(): Token {
|
|
@@ -713,48 +759,58 @@ export class Parser {
|
|
|
713
759
|
}
|
|
714
760
|
|
|
715
761
|
protected consume(type: TokenType, message: string): Token {
|
|
716
|
-
if (this.check(type))
|
|
717
|
-
|
|
718
|
-
|
|
762
|
+
if (this.check(type)) {
|
|
763
|
+
return this.advance();
|
|
764
|
+
}
|
|
765
|
+
// If we are in recovery mode (LSP + errorRecovery), report and return a synthetic token
|
|
766
|
+
if (this.mode === 'lsp' && this.options.errorRecovery) {
|
|
767
|
+
const tok = this.peek();
|
|
768
|
+
this.reportError(message, tok);
|
|
769
|
+
// Create a zero-width synthetic token of the expected type at the current position
|
|
770
|
+
const pos = this.isAtEnd() ? this.input.length : tok.start;
|
|
771
|
+
const synthetic: Token = {
|
|
772
|
+
type,
|
|
773
|
+
value: '',
|
|
774
|
+
start: pos,
|
|
775
|
+
end: pos,
|
|
776
|
+
line: tok?.line ?? 0,
|
|
777
|
+
column: tok?.column ?? 0,
|
|
778
|
+
range: {
|
|
779
|
+
start: { line: 0, character: pos, offset: pos },
|
|
780
|
+
end: { line: 0, character: pos, offset: pos }
|
|
781
|
+
}
|
|
782
|
+
} as Token;
|
|
783
|
+
return synthetic;
|
|
784
|
+
}
|
|
785
|
+
// Be lenient when cursor is provided and we're at EOF (legacy behavior for simple mode cursor tests)
|
|
719
786
|
if (this.options.cursorPosition !== undefined && this.isAtEnd()) {
|
|
720
|
-
|
|
721
|
-
|
|
787
|
+
const pos = this.input.length;
|
|
788
|
+
const synthetic: Token = {
|
|
722
789
|
type,
|
|
723
790
|
value: '',
|
|
724
|
-
start:
|
|
725
|
-
end:
|
|
791
|
+
start: pos,
|
|
792
|
+
end: pos,
|
|
793
|
+
line: 0,
|
|
794
|
+
column: 0,
|
|
795
|
+
range: {
|
|
796
|
+
start: { line: 0, character: pos, offset: pos },
|
|
797
|
+
end: { line: 0, character: pos, offset: pos }
|
|
798
|
+
}
|
|
726
799
|
} as Token;
|
|
800
|
+
return synthetic;
|
|
727
801
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const tokenStr = token.value || TokenType[token.type];
|
|
731
|
-
const range = this.getRangeFromToken(token);
|
|
732
|
-
const error = Errors.expectedToken(TokenType[type], tokenStr, range);
|
|
733
|
-
return this.handleError(error.message, token) as any;
|
|
802
|
+
// Simple mode: throw
|
|
803
|
+
this.throwSyntax(message, this.peek());
|
|
734
804
|
}
|
|
735
805
|
|
|
736
806
|
// Implement node creation methods
|
|
737
807
|
protected createIdentifierNode(name: string, token: Token): ASTNode {
|
|
738
808
|
const range = this.getRangeFromToken(token);
|
|
739
|
-
const isType = name[0] && name[0] >= 'A' && name[0] <= 'Z';
|
|
740
|
-
const nodeType = isType ? NodeType.TypeOrIdentifier : NodeType.Identifier;
|
|
741
|
-
|
|
742
809
|
const node: ASTNode = {
|
|
743
|
-
type:
|
|
810
|
+
type: NodeType.Identifier,
|
|
744
811
|
name,
|
|
745
812
|
range
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
// Add LSP features if in LSP mode
|
|
749
|
-
if (this.mode === 'lsp') {
|
|
750
|
-
this.enrichNodeForLSP(node, token);
|
|
751
|
-
|
|
752
|
-
// Index identifier
|
|
753
|
-
const identifiers = this.identifierIndex!.get(name) || [];
|
|
754
|
-
identifiers.push(node);
|
|
755
|
-
this.identifierIndex!.set(name, identifiers);
|
|
756
|
-
}
|
|
757
|
-
|
|
813
|
+
} as any;
|
|
758
814
|
return node;
|
|
759
815
|
}
|
|
760
816
|
|
|
@@ -766,9 +822,24 @@ export class Parser {
|
|
|
766
822
|
range: this.getRangeFromToken(token)
|
|
767
823
|
};
|
|
768
824
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
825
|
+
// LSP enrichment handled by augmentor
|
|
826
|
+
|
|
827
|
+
return node;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
protected createTemporalLiteralNode(rawValue: string, valueType: TemporalLiteralNode['valueType'], token: Token): TemporalLiteralNode {
|
|
831
|
+
// Parse temporal value immediately
|
|
832
|
+
const temporalValue = parseTemporalLiteral('@' + rawValue);
|
|
833
|
+
|
|
834
|
+
const node: TemporalLiteralNode = {
|
|
835
|
+
type: NodeType.TemporalLiteral,
|
|
836
|
+
value: temporalValue,
|
|
837
|
+
valueType,
|
|
838
|
+
rawValue,
|
|
839
|
+
range: this.getRangeFromToken(token)
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// LSP enrichment handled by augmentor
|
|
772
843
|
|
|
773
844
|
return node;
|
|
774
845
|
}
|
|
@@ -782,19 +853,7 @@ export class Parser {
|
|
|
782
853
|
range: this.getRangeFromNodes(left, right)
|
|
783
854
|
};
|
|
784
855
|
|
|
785
|
-
|
|
786
|
-
// For binary nodes, we need to find the actual start and end tokens
|
|
787
|
-
const startToken = this.tokens.find(t => t.start === left.range.start.offset) || token;
|
|
788
|
-
const endToken = this.tokens.find(t => t.end === right.range.end.offset) || token;
|
|
789
|
-
this.enrichNodeForLSP(node, startToken, endToken);
|
|
790
|
-
|
|
791
|
-
// Set up parent-child relationships
|
|
792
|
-
if (node.id) {
|
|
793
|
-
left.parent = node;
|
|
794
|
-
right.parent = node;
|
|
795
|
-
node.children = [left, right];
|
|
796
|
-
}
|
|
797
|
-
}
|
|
856
|
+
// LSP enrichment handled by augmentor
|
|
798
857
|
|
|
799
858
|
return node;
|
|
800
859
|
}
|
|
@@ -808,20 +867,12 @@ export class Parser {
|
|
|
808
867
|
range: { start: startPos, end: operand.range.end }
|
|
809
868
|
};
|
|
810
869
|
|
|
811
|
-
|
|
812
|
-
const endToken = this.tokens.find(t => t.end === operand.range.end.offset) || token;
|
|
813
|
-
this.enrichNodeForLSP(node, token, endToken);
|
|
814
|
-
|
|
815
|
-
if (node.id) {
|
|
816
|
-
operand.parent = node;
|
|
817
|
-
node.children = [operand];
|
|
818
|
-
}
|
|
819
|
-
}
|
|
870
|
+
// LSP enrichment handled by augmentor
|
|
820
871
|
|
|
821
872
|
return node;
|
|
822
873
|
}
|
|
823
874
|
|
|
824
|
-
protected createFunctionNode(name: ASTNode, args: ASTNode[]
|
|
875
|
+
protected createFunctionNode(name: ASTNode, args: ASTNode[]): FunctionNode {
|
|
825
876
|
const endNode = args.length > 0 ? args[args.length - 1]! : name;
|
|
826
877
|
const node: FunctionNode = {
|
|
827
878
|
type: NodeType.Function,
|
|
@@ -830,17 +881,7 @@ export class Parser {
|
|
|
830
881
|
range: this.getRangeFromNodes(name, endNode)
|
|
831
882
|
};
|
|
832
883
|
|
|
833
|
-
|
|
834
|
-
const startTok = this.tokens.find(t => t.start === name.range.start.offset) || startToken;
|
|
835
|
-
const endToken = this.tokens.find(t => t.end === endNode.range.end.offset) || startToken;
|
|
836
|
-
this.enrichNodeForLSP(node, startTok, endToken);
|
|
837
|
-
|
|
838
|
-
if (node.id) {
|
|
839
|
-
name.parent = node;
|
|
840
|
-
args.forEach(arg => { arg.parent = node; });
|
|
841
|
-
node.children = [name, ...args];
|
|
842
|
-
}
|
|
843
|
-
}
|
|
884
|
+
// LSP enrichment handled by augmentor
|
|
844
885
|
|
|
845
886
|
return node;
|
|
846
887
|
}
|
|
@@ -852,9 +893,7 @@ export class Parser {
|
|
|
852
893
|
range: this.getRangeFromToken(token)
|
|
853
894
|
};
|
|
854
895
|
|
|
855
|
-
|
|
856
|
-
this.enrichNodeForLSP(node, token);
|
|
857
|
-
}
|
|
896
|
+
// LSP enrichment handled by augmentor
|
|
858
897
|
|
|
859
898
|
return node;
|
|
860
899
|
}
|
|
@@ -867,64 +906,38 @@ export class Parser {
|
|
|
867
906
|
range: this.getRangeFromNodes(expression, index)
|
|
868
907
|
};
|
|
869
908
|
|
|
870
|
-
|
|
871
|
-
const startTok = this.tokens.find(t => t.start === expression.range.start.offset) || startToken;
|
|
872
|
-
const endToken = this.tokens.find(t => t.end === index.range.end.offset) || startToken;
|
|
873
|
-
this.enrichNodeForLSP(node, startTok, endToken);
|
|
874
|
-
|
|
875
|
-
if (node.id) {
|
|
876
|
-
expression.parent = node;
|
|
877
|
-
index.parent = node;
|
|
878
|
-
node.children = [expression, index];
|
|
879
|
-
}
|
|
880
|
-
}
|
|
909
|
+
// LSP enrichment handled by augmentor
|
|
881
910
|
|
|
882
911
|
return node;
|
|
883
912
|
}
|
|
884
913
|
|
|
885
914
|
|
|
886
915
|
protected createMembershipTestNode(expression: ASTNode, targetType: string, startToken: Token): MembershipTestNode {
|
|
887
|
-
// The range should extend from expression to the end of the type name
|
|
916
|
+
// The range should extend from expression start to the end of the type name
|
|
888
917
|
const endToken = this.previous(); // Should be the type identifier
|
|
889
918
|
const node: MembershipTestNode = {
|
|
890
919
|
type: NodeType.MembershipTest,
|
|
891
920
|
expression,
|
|
892
921
|
targetType,
|
|
893
|
-
range: this.
|
|
922
|
+
range: { start: expression.range.start, end: this.getRangeFromToken(endToken).end }
|
|
894
923
|
};
|
|
895
924
|
|
|
896
|
-
|
|
897
|
-
const startTok = this.tokens.find(t => t.start === expression.range.start.offset) || startToken;
|
|
898
|
-
this.enrichNodeForLSP(node, startTok, endToken);
|
|
899
|
-
|
|
900
|
-
if (node.id) {
|
|
901
|
-
expression.parent = node;
|
|
902
|
-
node.children = [expression];
|
|
903
|
-
}
|
|
904
|
-
}
|
|
925
|
+
// LSP enrichment handled by augmentor
|
|
905
926
|
|
|
906
927
|
return node;
|
|
907
928
|
}
|
|
908
929
|
|
|
909
930
|
protected createTypeCastNode(expression: ASTNode, targetType: string, startToken: Token): TypeCastNode {
|
|
910
|
-
// The range should extend from expression to the end of the type name
|
|
931
|
+
// The range should extend from expression start to the end of the type name
|
|
911
932
|
const endToken = this.previous(); // Should be the type identifier
|
|
912
933
|
const node: TypeCastNode = {
|
|
913
934
|
type: NodeType.TypeCast,
|
|
914
935
|
expression,
|
|
915
936
|
targetType,
|
|
916
|
-
range: this.
|
|
937
|
+
range: { start: expression.range.start, end: this.getRangeFromToken(endToken).end }
|
|
917
938
|
};
|
|
918
939
|
|
|
919
|
-
|
|
920
|
-
const startTok = this.tokens.find(t => t.start === expression.range.start.offset) || startToken;
|
|
921
|
-
this.enrichNodeForLSP(node, startTok, endToken);
|
|
922
|
-
|
|
923
|
-
if (node.id) {
|
|
924
|
-
expression.parent = node;
|
|
925
|
-
node.children = [expression];
|
|
926
|
-
}
|
|
927
|
-
}
|
|
940
|
+
// LSP enrichment handled by augmentor
|
|
928
941
|
|
|
929
942
|
return node;
|
|
930
943
|
}
|
|
@@ -937,80 +950,52 @@ export class Parser {
|
|
|
937
950
|
range: this.getRangeFromTokens(startToken, endToken)
|
|
938
951
|
};
|
|
939
952
|
|
|
940
|
-
|
|
941
|
-
this.enrichNodeForLSP(node, startToken, endToken);
|
|
942
|
-
|
|
943
|
-
if (node.id) {
|
|
944
|
-
elements.forEach(elem => { elem.parent = node; });
|
|
945
|
-
node.children = elements;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
953
|
+
// LSP enrichment handled by augmentor
|
|
948
954
|
|
|
949
955
|
return node;
|
|
950
956
|
}
|
|
951
957
|
|
|
952
|
-
protected createQuantityNode(value: number, unit: string,
|
|
958
|
+
protected createQuantityNode(value: number, unit: string, startToken: Token, endToken: Token): QuantityNode {
|
|
953
959
|
const node: QuantityNode = {
|
|
954
960
|
type: NodeType.Quantity,
|
|
955
961
|
value,
|
|
956
962
|
unit,
|
|
957
|
-
isCalendarUnit,
|
|
958
963
|
range: this.getRangeFromTokens(startToken, endToken)
|
|
959
964
|
};
|
|
960
965
|
|
|
961
|
-
|
|
962
|
-
this.enrichNodeForLSP(node, startToken, endToken);
|
|
963
|
-
}
|
|
966
|
+
// LSP enrichment handled by augmentor
|
|
964
967
|
|
|
965
968
|
return node;
|
|
966
969
|
}
|
|
967
970
|
|
|
968
|
-
|
|
971
|
+
// AST-level error handler: returns an ErrorNode in recovery, throws in simple mode
|
|
972
|
+
protected handleAstError(message: string, token?: Token): ErrorNode {
|
|
969
973
|
if (this.mode === 'lsp' && this.options.errorRecovery) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
// Try to synchronize
|
|
974
|
+
this.reportError(message, token);
|
|
975
|
+
// Attempt to synchronize so the parse can continue
|
|
974
976
|
this.synchronize();
|
|
975
|
-
|
|
976
|
-
// Return error node (cast to never to satisfy type system)
|
|
977
|
-
return this.createErrorNode(message, token) as never;
|
|
977
|
+
return this.createErrorNode(message, token);
|
|
978
978
|
}
|
|
979
|
-
|
|
980
|
-
|
|
979
|
+
this.throwSyntax(message, token);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Record an error (LSP path)
|
|
983
|
+
private reportError(message: string, token?: Token): void {
|
|
984
|
+
this.addError(message, token);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Throw a syntax error (simple mode path)
|
|
988
|
+
private throwSyntax(message: string, token?: Token): never {
|
|
981
989
|
const range = token ? this.getRangeFromToken(token) : undefined;
|
|
990
|
+
if (message.includes('Unexpected token:')) {
|
|
991
|
+
const tokenMatch = message.match(/Unexpected token: (.+)/);
|
|
992
|
+
const tokenValue = tokenMatch?.[1] ?? 'unknown';
|
|
993
|
+
throw Errors.unexpectedToken(tokenValue, range);
|
|
994
|
+
}
|
|
982
995
|
throw Errors.invalidSyntax(message, range);
|
|
983
996
|
}
|
|
984
997
|
|
|
985
|
-
// LSP
|
|
986
|
-
private enrichNodeForLSP(node: ASTNode, startToken: Token, endToken?: Token): void {
|
|
987
|
-
if (this.mode !== 'lsp') return;
|
|
988
|
-
|
|
989
|
-
// Add unique ID
|
|
990
|
-
node.id = `node_${this.nodeIdCounter!++}`;
|
|
991
|
-
|
|
992
|
-
// Add raw source text
|
|
993
|
-
const start = startToken.start;
|
|
994
|
-
const end = endToken ? endToken.end : startToken.end;
|
|
995
|
-
node.raw = this.input.substring(start, end);
|
|
996
|
-
|
|
997
|
-
// Add trivia if preserving
|
|
998
|
-
if (this.options.preserveTrivia) {
|
|
999
|
-
node.leadingTrivia = []; // TODO: Implement trivia collection
|
|
1000
|
-
node.trailingTrivia = [];
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Set parent relationship
|
|
1004
|
-
if (this.currentParent) {
|
|
1005
|
-
node.parent = this.currentParent;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Index node
|
|
1009
|
-
this.nodeIndex!.set(node.id, node);
|
|
1010
|
-
const nodesByType = this.nodesByType!.get(node.type) || [];
|
|
1011
|
-
nodesByType.push(node);
|
|
1012
|
-
this.nodesByType!.set(node.type, nodesByType);
|
|
1013
|
-
}
|
|
998
|
+
// LSP enrichment moved to augmentor
|
|
1014
999
|
|
|
1015
1000
|
private createErrorNode(message: string, token?: Token): ErrorNode {
|
|
1016
1001
|
const range = token ? this.getRangeFromToken(token) : {
|
|
@@ -1024,9 +1009,7 @@ export class Parser {
|
|
|
1024
1009
|
range
|
|
1025
1010
|
};
|
|
1026
1011
|
|
|
1027
|
-
|
|
1028
|
-
this.enrichNodeForLSP(node, token || this.peek());
|
|
1029
|
-
}
|
|
1012
|
+
// LSP enrichment handled by augmentor
|
|
1030
1013
|
|
|
1031
1014
|
return node;
|
|
1032
1015
|
}
|
|
@@ -1063,90 +1046,8 @@ export class Parser {
|
|
|
1063
1046
|
}
|
|
1064
1047
|
}
|
|
1065
1048
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
if (offset < root.range.start.offset! || offset > root.range.end.offset!) {
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Check children if they exist
|
|
1073
|
-
if ('children' in root && Array.isArray(root.children)) {
|
|
1074
|
-
for (const child of root.children) {
|
|
1075
|
-
const found = this.findNodeAtPosition(child, offset);
|
|
1076
|
-
if (found) return found;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Check specific node types
|
|
1081
|
-
if (root.type === NodeType.Binary) {
|
|
1082
|
-
const binaryNode = root as BinaryNode;
|
|
1083
|
-
const leftResult = this.findNodeAtPosition(binaryNode.left, offset);
|
|
1084
|
-
if (leftResult) return leftResult;
|
|
1085
|
-
const rightResult = this.findNodeAtPosition(binaryNode.right, offset);
|
|
1086
|
-
if (rightResult) return rightResult;
|
|
1087
|
-
} else if (root.type === NodeType.Unary) {
|
|
1088
|
-
const unaryNode = root as UnaryNode;
|
|
1089
|
-
return this.findNodeAtPosition(unaryNode.operand, offset);
|
|
1090
|
-
} else if (root.type === NodeType.Function) {
|
|
1091
|
-
const funcNode = root as FunctionNode;
|
|
1092
|
-
const nameResult = this.findNodeAtPosition(funcNode.name, offset);
|
|
1093
|
-
if (nameResult) return nameResult;
|
|
1094
|
-
for (const arg of funcNode.arguments) {
|
|
1095
|
-
const argResult = this.findNodeAtPosition(arg, offset);
|
|
1096
|
-
if (argResult) return argResult;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
return root;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
private getExpectedTokens(node: ASTNode | null): TokenType[] {
|
|
1104
|
-
if (!node) return this.getExpectedTokensForError();
|
|
1105
|
-
|
|
1106
|
-
// Context-specific expectations
|
|
1107
|
-
switch (node.type) {
|
|
1108
|
-
case NodeType.Binary:
|
|
1109
|
-
return [TokenType.DOT, TokenType.LBRACKET];
|
|
1110
|
-
case NodeType.Identifier:
|
|
1111
|
-
case NodeType.TypeOrIdentifier:
|
|
1112
|
-
return [TokenType.DOT, TokenType.LPAREN, TokenType.LBRACKET];
|
|
1113
|
-
default:
|
|
1114
|
-
return this.getExpectedTokensForError();
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
private getExpectedTokensForError(): TokenType[] {
|
|
1119
|
-
// Common continuations
|
|
1120
|
-
return [
|
|
1121
|
-
TokenType.EOF,
|
|
1122
|
-
TokenType.DOT,
|
|
1123
|
-
TokenType.LBRACKET,
|
|
1124
|
-
TokenType.LPAREN,
|
|
1125
|
-
TokenType.OPERATOR,
|
|
1126
|
-
TokenType.IDENTIFIER
|
|
1127
|
-
];
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
private getCompletions(node: ASTNode | null): string[] {
|
|
1131
|
-
if (!node) return [];
|
|
1132
|
-
|
|
1133
|
-
const completions: string[] = [];
|
|
1134
|
-
|
|
1135
|
-
// Add all identifiers seen so far
|
|
1136
|
-
if (this.identifierIndex) {
|
|
1137
|
-
for (const name of Array.from(this.identifierIndex.keys())) {
|
|
1138
|
-
completions.push(name);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// Add common FHIRPath functions
|
|
1143
|
-
completions.push(
|
|
1144
|
-
'where', 'select', 'first', 'last', 'tail',
|
|
1145
|
-
'skip', 'take', 'count', 'empty', 'exists'
|
|
1146
|
-
);
|
|
1147
|
-
|
|
1148
|
-
return completions;
|
|
1149
|
-
}
|
|
1049
|
+
// Cursor-specific transforms moved to augmentor
|
|
1050
|
+
// Cursor services moved to lsp/cursor-services
|
|
1150
1051
|
}
|
|
1151
1052
|
|
|
1152
1053
|
export function parse(input: string, options?: ParserOptions): ParseResult {
|
|
@@ -1160,146 +1061,4 @@ export function parse(input: string, options?: ParserOptions): ParseResult {
|
|
|
1160
1061
|
* @param indent - Current indentation level
|
|
1161
1062
|
* @returns Lisp-style string representation
|
|
1162
1063
|
*/
|
|
1163
|
-
|
|
1164
|
-
const spaces = ' '.repeat(indent);
|
|
1165
|
-
|
|
1166
|
-
switch (node.type) {
|
|
1167
|
-
case NodeType.Literal: {
|
|
1168
|
-
const lit = node as LiteralNode;
|
|
1169
|
-
if (lit.valueType === 'string') {
|
|
1170
|
-
return `"${lit.value}"`;
|
|
1171
|
-
} else if (lit.valueType === 'null') {
|
|
1172
|
-
return 'null';
|
|
1173
|
-
}
|
|
1174
|
-
return String(lit.value);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
case NodeType.Identifier:
|
|
1178
|
-
case NodeType.TypeOrIdentifier: {
|
|
1179
|
-
const id = node as IdentifierNode | TypeOrIdentifierNode;
|
|
1180
|
-
return id.name;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
case NodeType.Variable: {
|
|
1184
|
-
const v = node as VariableNode;
|
|
1185
|
-
return v.name;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
case NodeType.Binary: {
|
|
1189
|
-
const bin = node as BinaryNode;
|
|
1190
|
-
const op = bin.operator;
|
|
1191
|
-
|
|
1192
|
-
// For simple expressions, put on one line
|
|
1193
|
-
const leftStr = pprint(bin.left, 0);
|
|
1194
|
-
const rightStr = pprint(bin.right, 0);
|
|
1195
|
-
|
|
1196
|
-
if (leftStr.length + rightStr.length + op.length + 4 < 60 &&
|
|
1197
|
-
!leftStr.includes('\n') && !rightStr.includes('\n')) {
|
|
1198
|
-
return `(${op} ${leftStr} ${rightStr})`;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// For complex expressions, use multiple lines
|
|
1202
|
-
return `(${op}\n${spaces} ${pprint(bin.left, indent + 2)}\n${spaces} ${pprint(bin.right, indent + 2)})`;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
case NodeType.Unary: {
|
|
1206
|
-
const un = node as UnaryNode;
|
|
1207
|
-
const operandStr = pprint(un.operand, 0);
|
|
1208
|
-
|
|
1209
|
-
if (operandStr.length < 40 && !operandStr.includes('\n')) {
|
|
1210
|
-
return `(${un.operator} ${operandStr})`;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
return `(${un.operator}\n${spaces} ${pprint(un.operand, indent + 2)})`;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
case NodeType.Function: {
|
|
1217
|
-
const fn = node as FunctionNode;
|
|
1218
|
-
const nameStr = pprint(fn.name, 0);
|
|
1219
|
-
|
|
1220
|
-
if (fn.arguments.length === 0) {
|
|
1221
|
-
return `(${nameStr})`;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const argStrs = fn.arguments.map(arg => pprint(arg, 0));
|
|
1225
|
-
const totalLen = nameStr.length + argStrs.reduce((sum, s) => sum + s.length + 1, 0) + 2;
|
|
1226
|
-
|
|
1227
|
-
if (totalLen < 60 && argStrs.every(s => !s.includes('\n'))) {
|
|
1228
|
-
return `(${nameStr} ${argStrs.join(' ')})`;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// Multi-line format
|
|
1232
|
-
const argLines = fn.arguments.map(arg => `${spaces} ${pprint(arg, indent + 2)}`);
|
|
1233
|
-
return `(${nameStr}\n${argLines.join('\n')})`;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
case NodeType.Index: {
|
|
1237
|
-
const idx = node as IndexNode;
|
|
1238
|
-
const exprStr = pprint(idx.expression, 0);
|
|
1239
|
-
const indexStr = pprint(idx.index, 0);
|
|
1240
|
-
|
|
1241
|
-
if (exprStr.length + indexStr.length < 50 &&
|
|
1242
|
-
!exprStr.includes('\n') && !indexStr.includes('\n')) {
|
|
1243
|
-
return `([] ${exprStr} ${indexStr})`;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
return `([]\n${spaces} ${pprint(idx.expression, indent + 2)}\n${spaces} ${pprint(idx.index, indent + 2)})`;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
case NodeType.MembershipTest: {
|
|
1250
|
-
const mt = node as MembershipTestNode;
|
|
1251
|
-
const exprStr = pprint(mt.expression, 0);
|
|
1252
|
-
|
|
1253
|
-
if (exprStr.length + mt.targetType.length < 50 && !exprStr.includes('\n')) {
|
|
1254
|
-
return `(is ${exprStr} ${mt.targetType})`;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
return `(is\n${spaces} ${pprint(mt.expression, indent + 2)}\n${spaces} ${mt.targetType})`;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
case NodeType.TypeCast: {
|
|
1261
|
-
const tc = node as TypeCastNode;
|
|
1262
|
-
const exprStr = pprint(tc.expression, 0);
|
|
1263
|
-
|
|
1264
|
-
if (exprStr.length + tc.targetType.length < 50 && !exprStr.includes('\n')) {
|
|
1265
|
-
return `(as ${exprStr} ${tc.targetType})`;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
return `(as\n${spaces} ${pprint(tc.expression, indent + 2)}\n${spaces} ${tc.targetType})`;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
case NodeType.Collection: {
|
|
1272
|
-
const coll = node as CollectionNode;
|
|
1273
|
-
|
|
1274
|
-
if (coll.elements.length === 0) {
|
|
1275
|
-
return '{}';
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
const elemStrs = coll.elements.map(e => pprint(e, 0));
|
|
1279
|
-
const totalLen = elemStrs.reduce((sum, s) => sum + s.length + 1, 2);
|
|
1280
|
-
|
|
1281
|
-
if (totalLen < 60 && elemStrs.every(s => !s.includes('\n'))) {
|
|
1282
|
-
return `{${elemStrs.join(' ')}}`;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
const elemLines = coll.elements.map(e => `${spaces} ${pprint(e, indent + 2)}`);
|
|
1286
|
-
return `{\n${elemLines.join('\n')}\n${spaces}}`;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
case NodeType.TypeReference: {
|
|
1290
|
-
const tr = node as TypeReferenceNode;
|
|
1291
|
-
return `Type[${tr.typeName}]`;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
case NodeType.Quantity: {
|
|
1295
|
-
const q = node as QuantityNode;
|
|
1296
|
-
if (q.isCalendarUnit) {
|
|
1297
|
-
return `${q.value} ${q.unit}`;
|
|
1298
|
-
}
|
|
1299
|
-
return `${q.value} '${q.unit}'`;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
default:
|
|
1303
|
-
return `<unknown:${node.type}>`;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1064
|
+
// moved to src/utils/pprint
|