@atomic-ehr/fhirpath 0.0.1-canary.69eb286.20250724163205

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 (56) hide show
  1. package/README.md +307 -0
  2. package/dist/index.d.ts +225 -0
  3. package/dist/index.js +8256 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/analyzer/analyzer.ts +486 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +148 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +149 -0
  14. package/src/api/index.ts +57 -0
  15. package/src/api/registry.ts +128 -0
  16. package/src/api/types.ts +154 -0
  17. package/src/compiler/compiler.ts +589 -0
  18. package/src/compiler/index.ts +2 -0
  19. package/src/compiler/types.ts +23 -0
  20. package/src/index.ts +52 -0
  21. package/src/interpreter/README.md +78 -0
  22. package/src/interpreter/context.ts +181 -0
  23. package/src/interpreter/interpreter.ts +484 -0
  24. package/src/interpreter/types.ts +132 -0
  25. package/src/lexer/char-tables.ts +37 -0
  26. package/src/lexer/errors.ts +31 -0
  27. package/src/lexer/index.ts +5 -0
  28. package/src/lexer/lexer.ts +745 -0
  29. package/src/lexer/token.ts +104 -0
  30. package/src/parser/ast.ts +123 -0
  31. package/src/parser/index.ts +3 -0
  32. package/src/parser/parser.ts +701 -0
  33. package/src/parser/pprint.ts +169 -0
  34. package/src/registry/default-analyzers.ts +257 -0
  35. package/src/registry/default-compilers.ts +31 -0
  36. package/src/registry/index.ts +93 -0
  37. package/src/registry/operations/arithmetic.ts +506 -0
  38. package/src/registry/operations/collection.ts +422 -0
  39. package/src/registry/operations/comparison.ts +432 -0
  40. package/src/registry/operations/existence.ts +719 -0
  41. package/src/registry/operations/filtering.ts +374 -0
  42. package/src/registry/operations/literals.ts +341 -0
  43. package/src/registry/operations/logical.ts +402 -0
  44. package/src/registry/operations/math.ts +128 -0
  45. package/src/registry/operations/membership.ts +132 -0
  46. package/src/registry/operations/string.ts +507 -0
  47. package/src/registry/operations/subsetting.ts +174 -0
  48. package/src/registry/operations/type-checking.ts +162 -0
  49. package/src/registry/operations/type-conversion.ts +404 -0
  50. package/src/registry/operations/type-operators.ts +307 -0
  51. package/src/registry/operations/utility.ts +553 -0
  52. package/src/registry/registry.ts +146 -0
  53. package/src/registry/types.ts +162 -0
  54. package/src/registry/utils/evaluation-helpers.ts +93 -0
  55. package/src/registry/utils/index.ts +3 -0
  56. package/src/registry/utils/type-system.ts +173 -0
@@ -0,0 +1,484 @@
1
+ import type {
2
+ ASTNode,
3
+ LiteralNode,
4
+ IdentifierNode,
5
+ VariableNode,
6
+ BinaryNode,
7
+ UnaryNode,
8
+ FunctionNode,
9
+ CollectionNode,
10
+ IndexNode,
11
+ UnionNode,
12
+ MembershipTestNode,
13
+ TypeCastNode,
14
+ TypeReferenceNode,
15
+ TypeOrIdentifierNode
16
+ } from '../parser/ast';
17
+ import { NodeType } from '../parser/ast';
18
+ import { TokenType } from '../lexer/token';
19
+ import type { Context, EvaluationResult } from './types';
20
+ import { EvaluationError, CollectionUtils } from './types';
21
+ import { ContextManager } from './context';
22
+ import { TypeSystem } from '../registry/utils/type-system';
23
+ import { Registry } from '../registry';
24
+ import type { Interpreter as IInterpreter } from '../registry/types';
25
+
26
+ // Import registry to trigger operation registration
27
+ import '../registry';
28
+
29
+ // Type for node evaluator functions
30
+ type NodeEvaluator = (node: any, input: any[], context: Context) => EvaluationResult;
31
+
32
+ /**
33
+ * FHIRPath Interpreter - evaluates AST nodes following the stream-processing model.
34
+ * Every node is a processing unit: (input, context) → (output, new context)
35
+ *
36
+ * This refactored version uses object lookup instead of switch statements.
37
+ */
38
+ export class Interpreter implements IInterpreter {
39
+ // Object lookup for node evaluators
40
+ private readonly nodeEvaluators: Record<NodeType, NodeEvaluator> = {
41
+ [NodeType.Literal]: this.evaluateLiteral.bind(this),
42
+ [NodeType.Identifier]: this.evaluateIdentifier.bind(this),
43
+ [NodeType.TypeOrIdentifier]: this.evaluateTypeOrIdentifier.bind(this),
44
+ [NodeType.Variable]: this.evaluateVariable.bind(this),
45
+ [NodeType.Binary]: this.evaluateBinary.bind(this),
46
+ [NodeType.Unary]: this.evaluateUnary.bind(this),
47
+ [NodeType.Function]: this.evaluateFunction.bind(this),
48
+ [NodeType.Collection]: this.evaluateCollection.bind(this),
49
+ [NodeType.Index]: this.evaluateIndex.bind(this),
50
+ [NodeType.Union]: this.evaluateUnion.bind(this),
51
+ [NodeType.MembershipTest]: this.evaluateMembershipTest.bind(this),
52
+ [NodeType.TypeCast]: this.evaluateTypeCast.bind(this),
53
+ [NodeType.TypeReference]: this.evaluateTypeReference.bind(this),
54
+ };
55
+
56
+ /**
57
+ * Main evaluation method - uses object lookup instead of switch
58
+ */
59
+ evaluate(node: ASTNode, input: any[], context: Context): EvaluationResult {
60
+ try {
61
+ // Ensure $this is set in the context if not already present
62
+ if (!context.env.$this) {
63
+ context = {
64
+ ...context,
65
+ env: {
66
+ ...context.env,
67
+ $this: input
68
+ }
69
+ };
70
+ }
71
+
72
+ const evaluator = this.nodeEvaluators[node.type];
73
+
74
+ if (!evaluator) {
75
+ throw new EvaluationError(
76
+ `Unknown node type: ${node.type}`,
77
+ node.position
78
+ );
79
+ }
80
+
81
+ return evaluator(node, input, context);
82
+ } catch (error) {
83
+ // Add position information if not already present
84
+ if (error instanceof EvaluationError && !error.position && node.position) {
85
+ error.position = node.position;
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ private evaluateLiteral(node: LiteralNode, input: any[], context: Context): EvaluationResult {
92
+ // If literal has operation reference from parser
93
+ if (node.operation && node.operation.kind === 'literal') {
94
+ return node.operation.evaluate(this, context, input);
95
+ }
96
+
97
+ // Fallback for legacy literals
98
+ const value = node.value === null ? [] : [node.value];
99
+ return { value, context };
100
+ }
101
+
102
+ private evaluateIdentifier(node: IdentifierNode, input: any[], context: Context): EvaluationResult {
103
+ // Check if this identifier could be a resource type name
104
+ // Resource types in FHIR typically start with uppercase
105
+ if (node.name[0] === node?.name?.[0]?.toUpperCase()) {
106
+ // Check if any input items have this as their resourceType
107
+ const hasMatchingResourceType = input.some(item =>
108
+ item && typeof item === 'object' && item.resourceType === node.name
109
+ );
110
+
111
+ if (hasMatchingResourceType) {
112
+ // This is a type filter - return only items matching this resourceType
113
+ const filtered = input.filter(item =>
114
+ item && typeof item === 'object' && item.resourceType === node.name
115
+ );
116
+ return { value: filtered, context };
117
+ }
118
+ }
119
+
120
+ // Regular property navigation
121
+ const results: any[] = [];
122
+
123
+ for (const item of input) {
124
+ if (item == null || typeof item !== 'object') {
125
+ // Primitives don't have properties - skip
126
+ continue;
127
+ }
128
+
129
+ const value = item[node.name];
130
+ if (value !== undefined) {
131
+ // Add to results - flatten if array
132
+ if (Array.isArray(value)) {
133
+ results.push(...value);
134
+ } else {
135
+ results.push(value);
136
+ }
137
+ }
138
+ // Missing properties return empty (not added to results)
139
+ }
140
+
141
+ return { value: results, context };
142
+ }
143
+
144
+ private evaluateTypeOrIdentifier(node: TypeOrIdentifierNode, input: any[], context: Context): EvaluationResult {
145
+ // TypeOrIdentifier can act as either a type reference or property navigation
146
+
147
+ // First, check if this is a known type name (e.g., Patient, Observation)
148
+ // In FHIR context, type names match resourceType values
149
+ const possibleTypeName = node.name;
150
+
151
+ // Check if any input items have this as their resourceType
152
+ const hasMatchingResourceType = input.some(item =>
153
+ item && typeof item === 'object' && item.resourceType === possibleTypeName
154
+ );
155
+
156
+ if (hasMatchingResourceType) {
157
+ // This is a type filter - return only items matching this resourceType
158
+ const filtered = input.filter(item =>
159
+ item && typeof item === 'object' && item.resourceType === possibleTypeName
160
+ );
161
+ return { value: filtered, context };
162
+ }
163
+
164
+ // Not a type filter, treat as property navigation
165
+ return this.evaluateIdentifier(node as any, input, context);
166
+ }
167
+
168
+ private evaluateVariable(node: VariableNode, input: any[], context: Context): EvaluationResult {
169
+ // Variables ignore input and return value from context
170
+ let value: any[] = [];
171
+
172
+ if (node.name.startsWith('$')) {
173
+ // Special environment variables - use object lookup
174
+ const envVarHandlers: Record<string, () => any[]> = {
175
+ '$this': () => context.env.$this || [],
176
+ '$index': () => context.env.$index !== undefined ? [context.env.$index] : [],
177
+ '$total': () => context.env.$total || [],
178
+ };
179
+
180
+ const handler = envVarHandlers[node.name];
181
+ if (!handler) {
182
+ throw new EvaluationError(`Unknown special variable: ${node.name}`, node.position);
183
+ }
184
+ value = handler();
185
+ } else {
186
+ // User-defined variables (remove % prefix if present)
187
+ const varName = node.name.startsWith('%') ? node.name.substring(1) : node.name;
188
+ value = ContextManager.getVariable(context, varName) || [];
189
+ }
190
+
191
+ return { value, context };
192
+ }
193
+
194
+ private evaluateBinary(node: BinaryNode, input: any[], context: Context): EvaluationResult {
195
+ // Special handling for dot operator - it's a pipeline
196
+ if (node.operator === TokenType.DOT) {
197
+ // Phase 1: Evaluate left with original input/context
198
+ const leftResult = this.evaluate(node.left, input, context);
199
+
200
+ // Phase 2: Evaluate right with left's output as input
201
+ const rightResult = this.evaluate(node.right, leftResult.value, leftResult.context);
202
+
203
+ return rightResult;
204
+ }
205
+
206
+ // Handle case where parser incorrectly creates BinaryNode for unary minus
207
+ if (!node.left && !node.right && (node as any).operand) {
208
+ // This is actually a unary operation
209
+ const unaryOp = Registry.getByToken(node.operator, 'prefix');
210
+ if (unaryOp && unaryOp.kind === 'operator') {
211
+ const operandResult = this.evaluate((node as any).operand, input, context);
212
+ return unaryOp.evaluate(this, operandResult.context, input, operandResult.value);
213
+ }
214
+ }
215
+
216
+ // Get operation from registry (binary operators are infix)
217
+ const operation = node.operation || Registry.getByToken(node.operator, 'infix');
218
+ if (!operation || operation.kind !== 'operator') {
219
+ throw new EvaluationError(`Unknown operator: ${node.operator}`, node.position);
220
+ }
221
+
222
+ if (!node.left || !node.right) {
223
+ throw new EvaluationError(`Binary operator ${node.operator} missing operands`, node.position);
224
+ }
225
+
226
+ // Special handling for union operator - both sides should use the same context
227
+ if (node.operator === TokenType.PIPE) {
228
+ const leftResult = this.evaluate(node.left, input, context);
229
+ const rightResult = this.evaluate(node.right, input, context); // Use original context, not leftResult.context
230
+
231
+ // Use operation's evaluate method
232
+ return operation.evaluate(this, context, input, leftResult.value, rightResult.value);
233
+ }
234
+
235
+ // Normal operators - context flows from left to right
236
+ const leftResult = this.evaluate(node.left, input, context);
237
+ const rightResult = this.evaluate(node.right, input, leftResult.context);
238
+
239
+ // Use operation's evaluate method
240
+ return operation.evaluate(this, rightResult.context, input, leftResult.value, rightResult.value);
241
+ }
242
+
243
+ private evaluateUnary(node: UnaryNode, input: any[], context: Context): EvaluationResult {
244
+ // Get operation from registry (unary operators are prefix)
245
+ // Don't use node.operation as parser might have assigned wrong operation
246
+ const operation = Registry.getByToken(node.operator, 'prefix');
247
+ if (!operation || operation.kind !== 'operator') {
248
+ throw new EvaluationError(`Unknown unary operator: ${node.operator}`, node.position);
249
+ }
250
+
251
+ // Evaluate operand
252
+ const operandResult = this.evaluate(node.operand, input, context);
253
+
254
+ // Use operation's evaluate method
255
+ return operation.evaluate(this, operandResult.context, input, operandResult.value);
256
+ }
257
+
258
+ private evaluateFunction(node: FunctionNode, input: any[], context: Context): EvaluationResult {
259
+ // Extract function name and handle method call syntax
260
+ let funcName: string;
261
+ let functionInput = input;
262
+
263
+ if (node.name.type === NodeType.Identifier) {
264
+ funcName = (node.name as IdentifierNode).name;
265
+ } else if (node.name.type === NodeType.Binary && (node.name as BinaryNode).operator === TokenType.DOT) {
266
+ // Method call syntax: expression.function(args)
267
+ const binaryNode = node.name as BinaryNode;
268
+
269
+ // Evaluate the left side to get the input
270
+ const leftResult = this.evaluate(binaryNode.left, input, context);
271
+ functionInput = leftResult.value;
272
+ context = leftResult.context;
273
+
274
+ // Get the function name from the right side
275
+ if (binaryNode.right.type === NodeType.Identifier) {
276
+ funcName = (binaryNode.right as IdentifierNode).name;
277
+ } else {
278
+ throw new EvaluationError('Invalid method call syntax', node.position);
279
+ }
280
+ } else {
281
+ throw new EvaluationError('Complex function names not yet supported', node.position);
282
+ }
283
+
284
+ // Check for custom functions first
285
+ if (context.customFunctions && funcName in context.customFunctions) {
286
+ const customFunc = context.customFunctions[funcName];
287
+
288
+ // Evaluate all arguments
289
+ const evaluatedArgs: any[] = [];
290
+ for (const arg of node.arguments) {
291
+ const argResult = this.evaluate(arg, functionInput, context);
292
+ evaluatedArgs.push(argResult.value);
293
+ context = argResult.context;
294
+ }
295
+
296
+ // Call custom function
297
+ const result = customFunc!(context, functionInput, ...evaluatedArgs);
298
+ return { value: result, context };
299
+ }
300
+
301
+ // Get function from registry
302
+ const operation = Registry.get(funcName);
303
+ if (!operation || operation.kind !== 'function') {
304
+ throw new EvaluationError(`Unknown function: ${funcName}`, node.position);
305
+ }
306
+
307
+ // Check propagateEmptyInput flag
308
+ if (operation.signature.propagatesEmpty && functionInput.length === 0) {
309
+ return { value: [], context };
310
+ }
311
+
312
+ // Evaluate arguments based on parameter definitions
313
+ const evaluatedArgs: any[] = [];
314
+ for (let i = 0; i < node.arguments.length; i++) {
315
+ const arg = node.arguments[i];
316
+ const param = operation.signature.parameters[i];
317
+
318
+ if (param && param.kind === 'expression') {
319
+ // Pass expression as-is, will be evaluated by the function
320
+ evaluatedArgs.push(arg);
321
+ } else {
322
+ // Evaluate the argument to get its value
323
+ const argResult = this.evaluate(arg!, functionInput, context);
324
+ evaluatedArgs.push(argResult.value);
325
+ context = argResult.context;
326
+ }
327
+ }
328
+
329
+ // Use operation's evaluate method
330
+ return operation.evaluate(this, context, functionInput, ...evaluatedArgs);
331
+ }
332
+
333
+ private evaluateCollection(node: CollectionNode, input: any[], context: Context): EvaluationResult {
334
+ // Evaluate each element and combine results
335
+ const results: any[] = [];
336
+ let currentContext = context;
337
+
338
+ for (const element of node.elements) {
339
+ const result = this.evaluate(element, input, currentContext);
340
+ results.push(...result.value);
341
+ currentContext = result.context;
342
+ }
343
+
344
+ return { value: results, context: currentContext };
345
+ }
346
+
347
+ private evaluateIndex(node: IndexNode, input: any[], context: Context): EvaluationResult {
348
+ // Evaluate the expression being indexed
349
+ const exprResult = this.evaluate(node.expression, input, context);
350
+
351
+ // Evaluate the index expression in the original context
352
+ const indexResult = this.evaluate(node.index, input, context);
353
+
354
+ // Index must be a single integer
355
+ if (indexResult.value.length === 0) {
356
+ return { value: [], context: indexResult.context };
357
+ }
358
+
359
+ const index = CollectionUtils.toSingleton(indexResult.value);
360
+ if (typeof index !== 'number' || !Number.isInteger(index)) {
361
+ throw new EvaluationError('Index must be an integer', node.position);
362
+ }
363
+
364
+ // FHIRPath uses 0-based indexing
365
+ if (index < 0 || index >= exprResult.value.length) {
366
+ // Out of bounds returns empty
367
+ return { value: [], context: indexResult.context };
368
+ }
369
+
370
+ return { value: [exprResult.value[index]], context: indexResult.context };
371
+ }
372
+
373
+ private evaluateUnion(node: UnionNode, input: any[], context: Context): EvaluationResult {
374
+ // Union combines results from all operands
375
+ // Each operand should be evaluated with the SAME original context
376
+ // to prevent variable definitions from leaking between branches
377
+ const results: any[] = [];
378
+ const seen = new Set();
379
+
380
+ for (const operand of node.operands) {
381
+ // Always use the original context for each operand
382
+ const result = this.evaluate(operand, input, context);
383
+
384
+ // Remove duplicates
385
+ for (const item of result.value) {
386
+ const key = JSON.stringify(item);
387
+ if (!seen.has(key)) {
388
+ seen.add(key);
389
+ results.push(item);
390
+ }
391
+ }
392
+ }
393
+
394
+ // Return the original context, not a modified one
395
+ return { value: results, context };
396
+ }
397
+
398
+ private evaluateMembershipTest(node: MembershipTestNode, input: any[], context: Context): EvaluationResult {
399
+ // Evaluate the expression to get values to test
400
+ const exprResult = this.evaluate(node.expression, input, context);
401
+
402
+ // Empty collection: is returns empty
403
+ if (exprResult.value.length === 0) {
404
+ return { value: [], context: exprResult.context };
405
+ }
406
+
407
+ // Check if ALL values match the type
408
+ for (const value of exprResult.value) {
409
+ if (!TypeSystem.isType(value, node.targetType)) {
410
+ return { value: [false], context: exprResult.context };
411
+ }
412
+ }
413
+
414
+ // All values match the type
415
+ return { value: [true], context: exprResult.context };
416
+ }
417
+
418
+ private evaluateTypeCast(node: TypeCastNode, input: any[], context: Context): EvaluationResult {
419
+ // Evaluate the expression to get values to cast
420
+ const exprResult = this.evaluate(node.expression, input, context);
421
+
422
+ // For each value, attempt to cast to the target type
423
+ const results: any[] = [];
424
+ for (const value of exprResult.value) {
425
+ // If already the correct type, keep it
426
+ if (TypeSystem.isType(value, node.targetType)) {
427
+ results.push(value);
428
+ }
429
+ // Otherwise, try to cast (returns null if fails)
430
+ else {
431
+ const castValue = TypeSystem.cast(value, node.targetType);
432
+ if (castValue !== null) {
433
+ results.push(castValue);
434
+ }
435
+ // Failed casts are filtered out (not added to results)
436
+ }
437
+ }
438
+
439
+ // Return filtered collection
440
+ return { value: results, context: exprResult.context };
441
+ }
442
+
443
+ private evaluateTypeReference(node: TypeReferenceNode, input: any[], context: Context): EvaluationResult {
444
+ // Type references don't evaluate to values directly
445
+ throw new EvaluationError(`Type reference cannot be evaluated: ${node.typeName}`, node.position);
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Helper function to evaluate a FHIRPath expression
451
+ */
452
+ export function evaluateFHIRPath(
453
+ expression: string | ASTNode,
454
+ input: any,
455
+ context?: Context
456
+ ): any[] {
457
+ // Parse if string
458
+ const ast = typeof expression === 'string'
459
+ ? require('../parser').parse(expression)
460
+ : expression;
461
+
462
+ // Convert input to collection
463
+ const inputCollection = CollectionUtils.toCollection(input);
464
+
465
+ // Create context if not provided and set initial $this
466
+ let evalContext = context || ContextManager.create(inputCollection);
467
+
468
+ // Set initial $this to the input collection if not already set
469
+ if (!evalContext.env.$this) {
470
+ evalContext = {
471
+ ...evalContext,
472
+ env: {
473
+ ...evalContext.env,
474
+ $this: inputCollection
475
+ }
476
+ };
477
+ }
478
+
479
+ // Create interpreter and evaluate
480
+ const interpreter = new Interpreter();
481
+ const result = interpreter.evaluate(ast, inputCollection, evalContext);
482
+
483
+ return result.value;
484
+ }
@@ -0,0 +1,132 @@
1
+ // Core types for FHIRPath interpreter following the stream-processing mental model
2
+
3
+ /**
4
+ * The result of evaluating any FHIRPath expression.
5
+ * Every expression returns a collection and potentially modified context.
6
+ */
7
+ export interface EvaluationResult {
8
+ value: any[]; // Always a collection (even single values are collections of one)
9
+ context: Context;
10
+ }
11
+
12
+ /**
13
+ * Context carries variables and environment data parallel to the data stream.
14
+ * It flows through expressions and can be modified by certain operations.
15
+ *
16
+ * Uses JavaScript prototype chain for efficient inheritance.
17
+ */
18
+ export interface Context {
19
+ // User-defined variables (%varName)
20
+ // Using Record instead of Map for prototype chain compatibility
21
+ variables: Record<string, any[]>;
22
+
23
+ // Special environment variables
24
+ env: {
25
+ $this?: any[]; // Current item in iterator functions
26
+ $index?: number; // Current index in iterator functions
27
+ $total?: any[]; // Accumulator in aggregate function
28
+ };
29
+
30
+ // Root context variables
31
+ $context?: any[]; // Original input to the expression
32
+ $resource?: any[]; // Current resource being processed
33
+ $rootResource?: any[]; // Top-level resource
34
+
35
+ // Custom functions (if any)
36
+ customFunctions?: Record<string, (context: Context, input: any[], ...args: any[]) => any[]>;
37
+ }
38
+
39
+ /**
40
+ * Error thrown during evaluation with position information
41
+ */
42
+ export class EvaluationError extends Error {
43
+ constructor(
44
+ message: string,
45
+ public position?: { line: number; column: number; offset: number }
46
+ ) {
47
+ super(message);
48
+ this.name = 'EvaluationError';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Type information for runtime type checking
54
+ */
55
+ export interface TypeInfo {
56
+ namespace: string; // 'System' or 'FHIR'
57
+ name: string; // Type name like 'String', 'Patient'
58
+ isCollection?: boolean;
59
+ }
60
+
61
+ /**
62
+ * Helper type for ensuring we always work with collections
63
+ */
64
+ export type Collection<T = any> = T[];
65
+
66
+ /**
67
+ * Singleton conversion result
68
+ */
69
+ export type SingletonResult<T = any> = T | undefined;
70
+
71
+ /**
72
+ * Helper functions for working with collections
73
+ */
74
+ export const CollectionUtils = {
75
+ /**
76
+ * Convert any value to a collection
77
+ */
78
+ toCollection(value: any): any[] {
79
+ if (value === null || value === undefined) {
80
+ return [];
81
+ }
82
+ return Array.isArray(value) ? value : [value];
83
+ },
84
+
85
+ /**
86
+ * Apply singleton evaluation rules
87
+ * @returns The single value or undefined if rules don't apply
88
+ * @throws Error if multiple items when single expected
89
+ */
90
+ toSingleton(collection: any[], expectedType?: string): SingletonResult {
91
+ if (collection.length === 0) {
92
+ return undefined; // Empty propagates
93
+ }
94
+
95
+ if (collection.length === 1) {
96
+ const value = collection[0];
97
+
98
+ // Rule 2: Collection with one item, expecting Boolean → true
99
+ if (expectedType === 'boolean' && typeof value !== 'boolean') {
100
+ return true;
101
+ }
102
+
103
+ // Rule 1: Collection with one item convertible to expected type → use it
104
+ return value;
105
+ }
106
+
107
+ // Rule 4: Multiple items → ERROR
108
+ throw new EvaluationError(`Expected single value but got ${collection.length} items`);
109
+ },
110
+
111
+ /**
112
+ * Check if a collection is empty
113
+ */
114
+ isEmpty(collection: any[]): boolean {
115
+ return collection.length === 0;
116
+ },
117
+
118
+ /**
119
+ * Flatten nested collections
120
+ */
121
+ flatten(collection: any[]): any[] {
122
+ const result: any[] = [];
123
+ for (const item of collection) {
124
+ if (Array.isArray(item)) {
125
+ result.push(...item);
126
+ } else {
127
+ result.push(item);
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+ };
@@ -0,0 +1,37 @@
1
+ // Character classification lookup table for O(1) checks
2
+ export const CHAR_FLAGS = new Uint8Array(128);
3
+
4
+ // Bit flags for character properties
5
+ export const FLAG_DIGIT = 1 << 0;
6
+ export const FLAG_ALPHA = 1 << 1;
7
+ export const FLAG_WHITESPACE = 1 << 2;
8
+ export const FLAG_IDENTIFIER_START = 1 << 3;
9
+ export const FLAG_IDENTIFIER_CONT = 1 << 4;
10
+
11
+ // Initialize lookup table (called once at startup)
12
+ export function initCharTables(): void {
13
+ // Digits
14
+ for (let i = 48; i <= 57; i++) {
15
+ CHAR_FLAGS[i]! |= FLAG_DIGIT | FLAG_IDENTIFIER_CONT;
16
+ }
17
+
18
+ // Letters
19
+ for (let i = 65; i <= 90; i++) {
20
+ CHAR_FLAGS[i]! |= FLAG_ALPHA | FLAG_IDENTIFIER_START | FLAG_IDENTIFIER_CONT;
21
+ }
22
+ for (let i = 97; i <= 122; i++) {
23
+ CHAR_FLAGS[i]! |= FLAG_ALPHA | FLAG_IDENTIFIER_START | FLAG_IDENTIFIER_CONT;
24
+ }
25
+
26
+ // Underscore
27
+ CHAR_FLAGS[95]! |= FLAG_IDENTIFIER_START | FLAG_IDENTIFIER_CONT;
28
+
29
+ // Whitespace
30
+ CHAR_FLAGS[32]! |= FLAG_WHITESPACE; // space
31
+ CHAR_FLAGS[9]! |= FLAG_WHITESPACE; // tab
32
+ CHAR_FLAGS[10]! |= FLAG_WHITESPACE; // newline
33
+ CHAR_FLAGS[13]! |= FLAG_WHITESPACE; // carriage return
34
+ }
35
+
36
+ // Initialize the table immediately
37
+ initCharTables();
@@ -0,0 +1,31 @@
1
+ import type { Position } from './token';
2
+
3
+ export class LexerError extends Error {
4
+ constructor(
5
+ message: string,
6
+ public position: Position,
7
+ public char?: string
8
+ ) {
9
+ super(message);
10
+ this.name = 'LexerError';
11
+ }
12
+
13
+ override toString(): string {
14
+ const location = `${this.position.line}:${this.position.column}`;
15
+ const charInfo = this.char ? ` (found '${this.char}')` : '';
16
+ return `${this.name}: ${this.message} at ${location}${charInfo}`;
17
+ }
18
+ }
19
+
20
+ export function formatError(error: LexerError, input: string): string {
21
+ const lines = input.split('\n');
22
+ const line = lines[error.position.line - 1] || '';
23
+ const pointer = ' '.repeat(error.position.column - 1) + '^';
24
+
25
+ return [
26
+ error.toString(),
27
+ '',
28
+ line,
29
+ pointer
30
+ ].join('\n');
31
+ }
@@ -0,0 +1,5 @@
1
+ export { FHIRPathLexer, lex } from './lexer';
2
+ export type { Token, Position } from './token';
3
+ export { TokenType, Channel } from './token';
4
+ export { LexerError, formatError } from './errors';
5
+ export { CHAR_FLAGS, FLAG_DIGIT, FLAG_ALPHA, FLAG_WHITESPACE, FLAG_IDENTIFIER_START, FLAG_IDENTIFIER_CONT } from './char-tables';