@atomic-ehr/fhirpath 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
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, FHIRPathError, toDiagnostic } from './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
- // Filter out trivia tokens if they exist (tokens on hidden channel)
111
- if (lexerOptions.preserveTrivia) {
112
- this.tokens = this.tokens.filter(token =>
113
- token.channel === undefined || token.channel === Channel.DEFAULT
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
- this.nodeIdCounter = 0;
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
- private checkCursor(): AnyCursorNode | null {
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
- // Cursor is mid-token, ignore it
166
- return tokens;
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
- this.nodeIndex!.clear();
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 = this.findNodeAtPosition(ast, this.options.partialParse.cursorPosition);
307
+ const nodeAtCursor = findNodeAtPosition(aug.ast, this.options.partialParse.cursorPosition);
295
308
  result.cursorContext = {
296
309
  node: nodeAtCursor,
297
- expectedTokens: this.getExpectedTokens(nodeAtCursor),
298
- availableCompletions: this.getCompletions(nodeAtCursor)
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.advance();
333
- left = createCursorIndexNode(this.previous().start) as any;
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.current++; // inline advance()
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, false, token, nextToken);
460
+ return this.createQuantityNode(numberValue, unit, token, nextToken);
432
461
  }
433
462
 
434
463
  // Check if next token is a calendar duration identifier
@@ -438,7 +467,7 @@ 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, true, token, nextToken);
470
+ return this.createQuantityNode(numberValue, nextToken.value, token, nextToken);
442
471
  }
443
472
  }
444
473
 
@@ -461,16 +490,22 @@ export class Parser {
461
490
  return this.createLiteralNode(null, 'null', token);
462
491
  }
463
492
 
493
+ if (token.type === TokenType.DATE) {
494
+ this.advance();
495
+ const value = token.value.substring(1); // Remove @
496
+ return this.createTemporalLiteralNode(value, 'date', token);
497
+ }
498
+
464
499
  if (token.type === TokenType.DATETIME) {
465
500
  this.advance();
466
501
  const value = token.value.substring(1); // Remove @
467
- return this.createLiteralNode(value, 'datetime', token);
502
+ return this.createTemporalLiteralNode(value, 'datetime', token);
468
503
  }
469
504
 
470
505
  if (token.type === TokenType.TIME) {
471
506
  this.advance();
472
507
  const value = token.value.substring(1); // Remove @
473
- return this.createLiteralNode(value, 'time', token);
508
+ return this.createTemporalLiteralNode(value, 'time', token);
474
509
  }
475
510
 
476
511
  if (token.type === TokenType.SPECIAL_IDENTIFIER) {
@@ -490,6 +525,15 @@ export class Parser {
490
525
  }
491
526
 
492
527
  if (token.type === TokenType.IDENTIFIER) {
528
+ // If cursor is exactly at end of identifier and no whitespace at cursor, treat as identifier context
529
+ if (this.options.cursorPosition !== undefined && this.options.cursorPosition === token.end) {
530
+ const ch = this.input[this.options.cursorPosition];
531
+ const isWs = ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === undefined;
532
+ if (!isWs) {
533
+ this.advance();
534
+ return createCursorIdentifierNode(this.options.cursorPosition, token.value) as any;
535
+ }
536
+ }
493
537
  this.advance();
494
538
  const name = this.parseIdentifierValue(token.value);
495
539
  return this.createIdentifierNode(name, token);
@@ -519,7 +563,7 @@ export class Parser {
519
563
  const tokenStr = token.value || TokenType[token.type];
520
564
  const range = this.getRangeFromToken(token);
521
565
  const error = Errors.unexpectedToken(tokenStr, range);
522
- return this.handleError(error.message, token);
566
+ return this.handleAstError(error.message, token);
523
567
  }
524
568
 
525
569
  protected parseInvocation(): ASTNode {
@@ -533,17 +577,19 @@ export class Parser {
533
577
 
534
578
  // Allow identifiers and keywords that can be used as member names
535
579
  if (token.type === TokenType.IDENTIFIER) {
580
+ // Check for cursor at the end of an identifier
581
+ if (this.options.cursorPosition !== undefined && this.options.cursorPosition === token.end) {
582
+ this.advance();
583
+ return createCursorIdentifierNode(this.options.cursorPosition, token.value) as any;
584
+ }
585
+
536
586
  this.advance();
537
587
  const name = this.parseIdentifierValue(token.value);
538
588
  const node = this.createIdentifierNode(name, token);
539
589
 
540
590
  // Check if this is a function call
541
591
  if (this.check(TokenType.LPAREN)) {
542
- this.advance();
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);
592
+ return this.parseFunctionCall(node);
547
593
  }
548
594
 
549
595
  return node;
@@ -558,17 +604,17 @@ export class Parser {
558
604
  const tokenStr = token.value || TokenType[token.type];
559
605
  const range = this.getRangeFromToken(token);
560
606
  const error = Errors.expectedIdentifier('.', tokenStr, range);
561
- return this.handleError(error.message, token);
607
+ return this.handleAstError(error.message, token);
562
608
  }
563
609
 
564
- protected parseArgumentList(): ASTNode[] {
610
+ protected parseArgumentList(functionName?: string): ASTNode[] {
565
611
  const args: ASTNode[] = [];
566
612
 
567
613
  // Check for cursor at start of arguments
568
614
  if (this.peek().type === TokenType.CURSOR) {
569
615
  this.advance();
570
- // Need to get function name from context - for now use empty string
571
- args.push(createCursorArgumentNode(this.previous().start, '', 0) as any);
616
+ const fn = functionName ?? '';
617
+ args.push(createCursorArgumentNode(this.previous().start, fn, 0) as any);
572
618
  return args;
573
619
  }
574
620
 
@@ -582,7 +628,8 @@ export class Parser {
582
628
  // Check for cursor after comma
583
629
  if (this.peek().type === TokenType.CURSOR) {
584
630
  this.advance();
585
- args.push(createCursorArgumentNode(this.previous().start, '', args.length) as any);
631
+ const fn = functionName ?? '';
632
+ args.push(createCursorArgumentNode(this.previous().start, fn, args.length) as any);
586
633
  return args;
587
634
  }
588
635
  args.push(this.expression());
@@ -591,20 +638,29 @@ export class Parser {
591
638
  return args;
592
639
  }
593
640
 
641
+ // Shared function-call parser for both standalone and dotted calls
642
+ protected parseFunctionCall(nameNode: ASTNode): ASTNode {
643
+ // Current token is '(' per caller contract
644
+ this.advance();
645
+ const fnName = (nameNode as any)?.name && (typeof (nameNode as any).name === 'string')
646
+ ? (nameNode as any).name as string
647
+ : undefined;
648
+ const args = this.parseArgumentList(fnName);
649
+ this.consume(TokenType.RPAREN, "Expected ')'");
650
+ return this.createFunctionNode(nameNode, args);
651
+ }
652
+
594
653
  protected parseCollectionElements(): ASTNode[] {
595
654
  const elements: ASTNode[] = [];
596
655
 
597
656
  if (this.peek().type === TokenType.RBRACE) {
598
- return elements;
657
+ return elements; // Empty collection {} is valid
599
658
  }
600
659
 
601
- elements.push(this.expression());
602
-
603
- while (this.match(TokenType.COMMA)) {
604
- elements.push(this.expression());
605
- }
606
-
607
- return elements;
660
+ // Any other token is an error - braces can only contain empty collections
661
+ const unexpectedToken = this.peek();
662
+ 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)`;
663
+ return this.handleAstError(message, unexpectedToken) as any;
608
664
  }
609
665
 
610
666
  protected parseTypeName(): string {
@@ -613,8 +669,12 @@ export class Parser {
613
669
  const tokenStr = token.value || TokenType[token.type];
614
670
  const range = this.getRangeFromToken(token);
615
671
  const error = Errors.expectedTypeName(tokenStr, range);
616
- this.handleError(error.message, token);
617
- return ''; // For TypeScript, though handleError should throw/return error node
672
+ if (this.mode === 'lsp' && this.options.errorRecovery) {
673
+ this.reportError(error.message, token);
674
+ return '';
675
+ }
676
+ this.throwSyntax(error.message, token);
677
+ return '';
618
678
  }
619
679
  return this.parseIdentifierValue(token.value);
620
680
  }
@@ -648,36 +708,10 @@ export class Parser {
648
708
 
649
709
  // Shared utility methods
650
710
  protected isFunctionCall(node: ASTNode): boolean {
651
- return (node as any).type === NodeType.Identifier || (node as any).type === NodeType.TypeOrIdentifier;
711
+ return (node as any).type === NodeType.Identifier;
652
712
  }
653
713
 
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());
675
- }
676
-
677
- protected isKeywordAllowedAsIdentifier(token: Token): boolean {
678
- // Keywords that can be used as identifiers in certain contexts
679
- return this.isKeywordAllowedAsMember(token);
680
- }
714
+ // removed unused: isBinaryOperatorToken, isKeywordAllowedAsMember, isKeywordAllowedAsIdentifier
681
715
 
682
716
  // Helper methods
683
717
  protected peek(): Token {
@@ -713,48 +747,58 @@ export class Parser {
713
747
  }
714
748
 
715
749
  protected consume(type: TokenType, message: string): Token {
716
- if (this.check(type)) return this.advance();
717
-
718
- // Be lenient when cursor is present and we're at EOF
750
+ if (this.check(type)) {
751
+ return this.advance();
752
+ }
753
+ // If we are in recovery mode (LSP + errorRecovery), report and return a synthetic token
754
+ if (this.mode === 'lsp' && this.options.errorRecovery) {
755
+ const tok = this.peek();
756
+ this.reportError(message, tok);
757
+ // Create a zero-width synthetic token of the expected type at the current position
758
+ const pos = this.isAtEnd() ? this.input.length : tok.start;
759
+ const synthetic: Token = {
760
+ type,
761
+ value: '',
762
+ start: pos,
763
+ end: pos,
764
+ line: tok?.line ?? 0,
765
+ column: tok?.column ?? 0,
766
+ range: {
767
+ start: { line: 0, character: pos, offset: pos },
768
+ end: { line: 0, character: pos, offset: pos }
769
+ }
770
+ } as Token;
771
+ return synthetic;
772
+ }
773
+ // Be lenient when cursor is provided and we're at EOF (legacy behavior for simple mode cursor tests)
719
774
  if (this.options.cursorPosition !== undefined && this.isAtEnd()) {
720
- // Return a synthetic token to allow parsing to continue
721
- return {
775
+ const pos = this.input.length;
776
+ const synthetic: Token = {
722
777
  type,
723
778
  value: '',
724
- start: this.input.length,
725
- end: this.input.length
779
+ start: pos,
780
+ end: pos,
781
+ line: 0,
782
+ column: 0,
783
+ range: {
784
+ start: { line: 0, character: pos, offset: pos },
785
+ end: { line: 0, character: pos, offset: pos }
786
+ }
726
787
  } as Token;
788
+ return synthetic;
727
789
  }
728
-
729
- const token = this.peek();
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;
790
+ // Simple mode: throw
791
+ this.throwSyntax(message, this.peek());
734
792
  }
735
793
 
736
794
  // Implement node creation methods
737
795
  protected createIdentifierNode(name: string, token: Token): ASTNode {
738
796
  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
797
  const node: ASTNode = {
743
- type: nodeType,
798
+ type: NodeType.Identifier,
744
799
  name,
745
800
  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
-
801
+ } as any;
758
802
  return node;
759
803
  }
760
804
 
@@ -766,9 +810,24 @@ export class Parser {
766
810
  range: this.getRangeFromToken(token)
767
811
  };
768
812
 
769
- if (this.mode === 'lsp') {
770
- this.enrichNodeForLSP(node, token);
771
- }
813
+ // LSP enrichment handled by augmentor
814
+
815
+ return node;
816
+ }
817
+
818
+ protected createTemporalLiteralNode(rawValue: string, valueType: TemporalLiteralNode['valueType'], token: Token): TemporalLiteralNode {
819
+ // Parse temporal value immediately
820
+ const temporalValue = parseTemporalLiteral('@' + rawValue);
821
+
822
+ const node: TemporalLiteralNode = {
823
+ type: NodeType.TemporalLiteral,
824
+ value: temporalValue,
825
+ valueType,
826
+ rawValue,
827
+ range: this.getRangeFromToken(token)
828
+ };
829
+
830
+ // LSP enrichment handled by augmentor
772
831
 
773
832
  return node;
774
833
  }
@@ -782,19 +841,7 @@ export class Parser {
782
841
  range: this.getRangeFromNodes(left, right)
783
842
  };
784
843
 
785
- if (this.mode === 'lsp') {
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
- }
844
+ // LSP enrichment handled by augmentor
798
845
 
799
846
  return node;
800
847
  }
@@ -808,20 +855,12 @@ export class Parser {
808
855
  range: { start: startPos, end: operand.range.end }
809
856
  };
810
857
 
811
- if (this.mode === 'lsp') {
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
- }
858
+ // LSP enrichment handled by augmentor
820
859
 
821
860
  return node;
822
861
  }
823
862
 
824
- protected createFunctionNode(name: ASTNode, args: ASTNode[], startToken: Token): FunctionNode {
863
+ protected createFunctionNode(name: ASTNode, args: ASTNode[]): FunctionNode {
825
864
  const endNode = args.length > 0 ? args[args.length - 1]! : name;
826
865
  const node: FunctionNode = {
827
866
  type: NodeType.Function,
@@ -830,17 +869,7 @@ export class Parser {
830
869
  range: this.getRangeFromNodes(name, endNode)
831
870
  };
832
871
 
833
- if (this.mode === 'lsp') {
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
- }
872
+ // LSP enrichment handled by augmentor
844
873
 
845
874
  return node;
846
875
  }
@@ -852,9 +881,7 @@ export class Parser {
852
881
  range: this.getRangeFromToken(token)
853
882
  };
854
883
 
855
- if (this.mode === 'lsp') {
856
- this.enrichNodeForLSP(node, token);
857
- }
884
+ // LSP enrichment handled by augmentor
858
885
 
859
886
  return node;
860
887
  }
@@ -867,64 +894,38 @@ export class Parser {
867
894
  range: this.getRangeFromNodes(expression, index)
868
895
  };
869
896
 
870
- if (this.mode === 'lsp') {
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
- }
897
+ // LSP enrichment handled by augmentor
881
898
 
882
899
  return node;
883
900
  }
884
901
 
885
902
 
886
903
  protected createMembershipTestNode(expression: ASTNode, targetType: string, startToken: Token): MembershipTestNode {
887
- // The range should extend from expression to the end of the type name
904
+ // The range should extend from expression start to the end of the type name
888
905
  const endToken = this.previous(); // Should be the type identifier
889
906
  const node: MembershipTestNode = {
890
907
  type: NodeType.MembershipTest,
891
908
  expression,
892
909
  targetType,
893
- range: this.getRangeFromTokens(startToken, endToken)
910
+ range: { start: expression.range.start, end: this.getRangeFromToken(endToken).end }
894
911
  };
895
912
 
896
- if (this.mode === 'lsp') {
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
- }
913
+ // LSP enrichment handled by augmentor
905
914
 
906
915
  return node;
907
916
  }
908
917
 
909
918
  protected createTypeCastNode(expression: ASTNode, targetType: string, startToken: Token): TypeCastNode {
910
- // The range should extend from expression to the end of the type name
919
+ // The range should extend from expression start to the end of the type name
911
920
  const endToken = this.previous(); // Should be the type identifier
912
921
  const node: TypeCastNode = {
913
922
  type: NodeType.TypeCast,
914
923
  expression,
915
924
  targetType,
916
- range: this.getRangeFromTokens(startToken, endToken)
925
+ range: { start: expression.range.start, end: this.getRangeFromToken(endToken).end }
917
926
  };
918
927
 
919
- if (this.mode === 'lsp') {
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
- }
928
+ // LSP enrichment handled by augmentor
928
929
 
929
930
  return node;
930
931
  }
@@ -937,80 +938,52 @@ export class Parser {
937
938
  range: this.getRangeFromTokens(startToken, endToken)
938
939
  };
939
940
 
940
- if (this.mode === 'lsp') {
941
- this.enrichNodeForLSP(node, startToken, endToken);
942
-
943
- if (node.id) {
944
- elements.forEach(elem => { elem.parent = node; });
945
- node.children = elements;
946
- }
947
- }
941
+ // LSP enrichment handled by augmentor
948
942
 
949
943
  return node;
950
944
  }
951
945
 
952
- protected createQuantityNode(value: number, unit: string, isCalendarUnit: boolean, startToken: Token, endToken: Token): QuantityNode {
946
+ protected createQuantityNode(value: number, unit: string, startToken: Token, endToken: Token): QuantityNode {
953
947
  const node: QuantityNode = {
954
948
  type: NodeType.Quantity,
955
949
  value,
956
950
  unit,
957
- isCalendarUnit,
958
951
  range: this.getRangeFromTokens(startToken, endToken)
959
952
  };
960
953
 
961
- if (this.mode === 'lsp') {
962
- this.enrichNodeForLSP(node, startToken, endToken);
963
- }
954
+ // LSP enrichment handled by augmentor
964
955
 
965
956
  return node;
966
957
  }
967
958
 
968
- protected handleError(message: string, token?: Token): never {
959
+ // AST-level error handler: returns an ErrorNode in recovery, throws in simple mode
960
+ protected handleAstError(message: string, token?: Token): ErrorNode {
969
961
  if (this.mode === 'lsp' && this.options.errorRecovery) {
970
- // In LSP mode with error recovery, add error and try to recover
971
- this.addError(message, token);
972
-
973
- // Try to synchronize
962
+ this.reportError(message, token);
963
+ // Attempt to synchronize so the parse can continue
974
964
  this.synchronize();
975
-
976
- // Return error node (cast to never to satisfy type system)
977
- return this.createErrorNode(message, token) as never;
965
+ return this.createErrorNode(message, token);
978
966
  }
979
-
980
- // In simple mode, throw error
967
+ this.throwSyntax(message, token);
968
+ }
969
+
970
+ // Record an error (LSP path)
971
+ private reportError(message: string, token?: Token): void {
972
+ this.addError(message, token);
973
+ }
974
+
975
+ // Throw a syntax error (simple mode path)
976
+ private throwSyntax(message: string, token?: Token): never {
981
977
  const range = token ? this.getRangeFromToken(token) : undefined;
978
+ if (message.includes('Unexpected token:')) {
979
+ const tokenMatch = message.match(/Unexpected token: (.+)/);
980
+ const tokenValue = tokenMatch?.[1] ?? 'unknown';
981
+ throw Errors.unexpectedToken(tokenValue, range);
982
+ }
982
983
  throw Errors.invalidSyntax(message, range);
983
984
  }
984
985
 
985
- // LSP mode helper methods
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
- }
986
+ // LSP enrichment moved to augmentor
1014
987
 
1015
988
  private createErrorNode(message: string, token?: Token): ErrorNode {
1016
989
  const range = token ? this.getRangeFromToken(token) : {
@@ -1024,9 +997,7 @@ export class Parser {
1024
997
  range
1025
998
  };
1026
999
 
1027
- if (this.mode === 'lsp') {
1028
- this.enrichNodeForLSP(node, token || this.peek());
1029
- }
1000
+ // LSP enrichment handled by augmentor
1030
1001
 
1031
1002
  return node;
1032
1003
  }
@@ -1063,90 +1034,8 @@ export class Parser {
1063
1034
  }
1064
1035
  }
1065
1036
 
1066
- private findNodeAtPosition(root: ASTNode, offset: number): ASTNode | null {
1067
- // DFS to find the most specific node containing the position
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
- }
1037
+ // Cursor-specific transforms moved to augmentor
1038
+ // Cursor services moved to lsp/cursor-services
1150
1039
  }
1151
1040
 
1152
1041
  export function parse(input: string, options?: ParserOptions): ParseResult {
@@ -1160,146 +1049,4 @@ export function parse(input: string, options?: ParserOptions): ParseResult {
1160
1049
  * @param indent - Current indentation level
1161
1050
  * @returns Lisp-style string representation
1162
1051
  */
1163
- export function pprint(node: ASTNode, indent: number = 0): string {
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
- }
1052
+ // moved to src/utils/pprint