@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,553 @@
1
+ import type { Function } from '../types';
2
+ import { defaultFunctionAnalyze } from '../default-analyzers';
3
+ import { defaultFunctionCompile } from '../default-compilers';
4
+ import { ContextManager } from '../../interpreter/context';
5
+ import { isTruthy } from '../utils';
6
+
7
+ export const aggregateFunction: Function = {
8
+ name: 'aggregate',
9
+ kind: 'function',
10
+
11
+ syntax: {
12
+ notation: 'aggregate(aggregator, init)'
13
+ },
14
+
15
+ signature: {
16
+ input: {
17
+ types: { kind: 'any' },
18
+ cardinality: 'collection'
19
+ },
20
+ parameters: [
21
+ {
22
+ name: 'aggregator',
23
+ kind: 'expression',
24
+ types: { kind: 'any' },
25
+ cardinality: 'collection',
26
+ optional: false
27
+ },
28
+ {
29
+ name: 'init',
30
+ kind: 'value',
31
+ types: { kind: 'any' },
32
+ cardinality: 'collection',
33
+ optional: true
34
+ }
35
+ ],
36
+ output: {
37
+ type: 'any',
38
+ cardinality: 'collection'
39
+ },
40
+ propagatesEmpty: false,
41
+ deterministic: true
42
+ },
43
+
44
+ analyze: defaultFunctionAnalyze,
45
+
46
+ evaluate: (interpreter, context, input, aggregatorExpr, init) => {
47
+ if (!aggregatorExpr) {
48
+ throw new Error('aggregate() requires an aggregator expression');
49
+ }
50
+
51
+ let total = init !== undefined ? init : [];
52
+
53
+ for (let i = 0; i < input.length; i++) {
54
+ const item = input[i];
55
+ const iterContext = {
56
+ ...ContextManager.setIteratorContext(context, item, i),
57
+ env: {
58
+ ...context.env,
59
+ $this: [item],
60
+ $index: i,
61
+ $total: total
62
+ }
63
+ };
64
+
65
+ const result = interpreter.evaluate(aggregatorExpr, [item], iterContext);
66
+ total = result.value;
67
+ }
68
+
69
+ return { value: total, context };
70
+ },
71
+
72
+ compile: defaultFunctionCompile
73
+ };
74
+
75
+ export const childrenFunction: Function = {
76
+ name: 'children',
77
+ kind: 'function',
78
+
79
+ syntax: {
80
+ notation: 'children()'
81
+ },
82
+
83
+ signature: {
84
+ input: {
85
+ types: { kind: 'any' },
86
+ cardinality: 'collection'
87
+ },
88
+ parameters: [],
89
+ output: {
90
+ type: 'preserve-input',
91
+ cardinality: 'collection'
92
+ },
93
+ propagatesEmpty: true,
94
+ deterministic: true
95
+ },
96
+
97
+ analyze: defaultFunctionAnalyze,
98
+
99
+ evaluate: (interpreter, context, input) => {
100
+ const results: any[] = [];
101
+
102
+ for (const item of input) {
103
+ if (item && typeof item === 'object') {
104
+ for (const key of Object.keys(item)) {
105
+ if (!key.startsWith('_')) {
106
+ const value = item[key];
107
+ if (Array.isArray(value)) {
108
+ results.push(...value);
109
+ } else if (value !== null && value !== undefined) {
110
+ results.push(value);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return { value: results, context };
118
+ },
119
+
120
+ compile: defaultFunctionCompile
121
+ };
122
+
123
+ export const descendantsFunction: Function = {
124
+ name: 'descendants',
125
+ kind: 'function',
126
+
127
+ syntax: {
128
+ notation: 'descendants()'
129
+ },
130
+
131
+ signature: {
132
+ input: {
133
+ types: { kind: 'any' },
134
+ cardinality: 'collection'
135
+ },
136
+ parameters: [],
137
+ output: {
138
+ type: 'preserve-input',
139
+ cardinality: 'collection'
140
+ },
141
+ propagatesEmpty: true,
142
+ deterministic: true
143
+ },
144
+
145
+ analyze: defaultFunctionAnalyze,
146
+
147
+ evaluate: (interpreter, context, input) => {
148
+ const results: any[] = [];
149
+ const seen = new Set();
150
+
151
+ function collectDescendants(items: any[]) {
152
+ for (const item of items) {
153
+ if (item && typeof item === 'object') {
154
+ const key = JSON.stringify(item);
155
+ if (!seen.has(key)) {
156
+ seen.add(key);
157
+ results.push(item);
158
+
159
+ const children: any[] = [];
160
+ for (const prop of Object.keys(item)) {
161
+ if (!prop.startsWith('_')) {
162
+ const value = item[prop];
163
+ if (Array.isArray(value)) {
164
+ children.push(...value);
165
+ } else if (value !== null && value !== undefined && typeof value === 'object') {
166
+ children.push(value);
167
+ }
168
+ }
169
+ }
170
+ collectDescendants(children);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ const children: any[] = [];
177
+ for (const item of input) {
178
+ if (item && typeof item === 'object') {
179
+ for (const key of Object.keys(item)) {
180
+ if (!key.startsWith('_')) {
181
+ const value = item[key];
182
+ if (Array.isArray(value)) {
183
+ children.push(...value);
184
+ } else if (value !== null && value !== undefined) {
185
+ children.push(value);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ collectDescendants(children);
193
+ return { value: results, context };
194
+ },
195
+
196
+ compile: defaultFunctionCompile
197
+ };
198
+
199
+ export const iifFunction: Function = {
200
+ name: 'iif',
201
+ kind: 'function',
202
+
203
+ syntax: {
204
+ notation: 'iif(condition, then, else)'
205
+ },
206
+
207
+ signature: {
208
+ input: {
209
+ types: { kind: 'any' },
210
+ cardinality: 'collection'
211
+ },
212
+ parameters: [
213
+ {
214
+ name: 'condition',
215
+ kind: 'expression',
216
+ types: { kind: 'any' },
217
+ cardinality: 'collection',
218
+ optional: false
219
+ },
220
+ {
221
+ name: 'then',
222
+ kind: 'expression',
223
+ types: { kind: 'any' },
224
+ cardinality: 'collection',
225
+ optional: false
226
+ },
227
+ {
228
+ name: 'else',
229
+ kind: 'expression',
230
+ types: { kind: 'any' },
231
+ cardinality: 'collection',
232
+ optional: false
233
+ }
234
+ ],
235
+ output: {
236
+ type: 'any',
237
+ cardinality: 'collection'
238
+ },
239
+ propagatesEmpty: false,
240
+ deterministic: true
241
+ },
242
+
243
+ analyze: defaultFunctionAnalyze,
244
+
245
+ evaluate: (interpreter, context, input, condition, thenBranch, elseBranch) => {
246
+ const condResult = interpreter.evaluate(condition, input, context);
247
+
248
+ if (isTruthy(condResult.value)) {
249
+ return interpreter.evaluate(thenBranch, input, condResult.context);
250
+ } else {
251
+ return interpreter.evaluate(elseBranch, input, condResult.context);
252
+ }
253
+ },
254
+
255
+ compile: (compiler, input, args) => {
256
+ const [condExpr, thenExpr, elseExpr] = args;
257
+
258
+ if (!condExpr || !thenExpr || !elseExpr) {
259
+ throw new Error('iif() requires condition, then, and else expressions');
260
+ }
261
+
262
+ return {
263
+ fn: (ctx) => {
264
+ const inputVal = input.fn(ctx);
265
+ const condCtx = { ...ctx, input: inputVal, focus: inputVal };
266
+ const condResult = condExpr.fn(condCtx);
267
+
268
+ if (isTruthy(condResult)) {
269
+ const thenCtx = { ...ctx, input: inputVal, focus: inputVal };
270
+ return thenExpr.fn(thenCtx);
271
+ } else {
272
+ const elseCtx = { ...ctx, input: inputVal, focus: inputVal };
273
+ return elseExpr.fn(elseCtx);
274
+ }
275
+ },
276
+ type: compiler.resolveType('Any'),
277
+ isSingleton: false,
278
+ source: `${input.source || ''}.iif(${condExpr.source || ''}, ${thenExpr.source || ''}, ${elseExpr.source || ''})`
279
+ };
280
+ }
281
+ };
282
+
283
+ export const defineVariableFunction: Function = {
284
+ name: 'defineVariable',
285
+ kind: 'function',
286
+
287
+ syntax: {
288
+ notation: 'defineVariable(name [, value])'
289
+ },
290
+
291
+ signature: {
292
+ input: {
293
+ types: { kind: 'any' },
294
+ cardinality: 'collection'
295
+ },
296
+ parameters: [
297
+ {
298
+ name: 'name',
299
+ kind: 'value',
300
+ types: { kind: 'primitive', types: ['String'] },
301
+ cardinality: 'singleton',
302
+ optional: false
303
+ },
304
+ {
305
+ name: 'value',
306
+ kind: 'expression',
307
+ types: { kind: 'any' },
308
+ cardinality: 'collection',
309
+ optional: true
310
+ }
311
+ ],
312
+ output: {
313
+ type: 'preserve-input',
314
+ cardinality: 'collection'
315
+ },
316
+ propagatesEmpty: false,
317
+ deterministic: true
318
+ },
319
+
320
+ analyze: defaultFunctionAnalyze,
321
+
322
+ evaluate: (interpreter, context, input, nameValue, valueExpr) => {
323
+ // Extract the string value from the name parameter
324
+ let varName: string;
325
+
326
+ // nameValue comes as evaluated value (array) when param.kind !== 'expression'
327
+ if (Array.isArray(nameValue) && nameValue.length === 1 && typeof nameValue[0] === 'string') {
328
+ varName = nameValue[0];
329
+ } else if (typeof nameValue === 'string') {
330
+ varName = nameValue;
331
+ } else {
332
+ throw new Error('defineVariable() requires a string literal as the first parameter');
333
+ }
334
+
335
+ // Check if the variable is a system variable
336
+ const systemVariables = ['context', 'resource', 'rootResource', 'ucum', 'sct', 'loinc'];
337
+ if (systemVariables.includes(varName)) {
338
+ // Return empty result for system variable redefinition
339
+ return { value: [], context };
340
+ }
341
+
342
+ // Check if the variable is already defined at the current scope level
343
+ // We only check hasOwnProperty to detect redefinition at the same scope
344
+ if (context.variables && Object.prototype.hasOwnProperty.call(context.variables, varName)) {
345
+ // Return empty result for variable redefinition in the same scope
346
+ return { value: [], context };
347
+ }
348
+
349
+ let value: any[];
350
+
351
+ if (valueExpr) {
352
+ // Create a new context where $this refers to the input
353
+ const valueContext = ContextManager.copy(context);
354
+ valueContext.env = { ...valueContext.env, $this: input };
355
+
356
+ const result = interpreter.evaluate(valueExpr, input, valueContext);
357
+ value = result.value;
358
+ const newContext = ContextManager.setVariable(result.context, varName, value);
359
+ return { value: input, context: newContext };
360
+ } else {
361
+ // If no value expression is provided, use the input collection
362
+ value = input;
363
+ const newContext = ContextManager.setVariable(context, varName, value);
364
+ return { value: input, context: newContext };
365
+ }
366
+ },
367
+
368
+ compile: (compiler, input, args) => {
369
+ const [nameExpr, valueExpr] = args;
370
+
371
+ // For defineVariable, the name should be a literal string
372
+ // Try to extract it at compile time
373
+ let varName: string | undefined;
374
+
375
+ // The name parameter should evaluate to a constant string
376
+ try {
377
+ const nameResult = nameExpr?.fn({ input: [], focus: [], env: {} }) || [];
378
+ if (nameResult.length === 1 && typeof nameResult[0] === 'string') {
379
+ varName = nameResult[0];
380
+ }
381
+ } catch (e) {
382
+ // If we can't evaluate it at compile time, it might be invalid
383
+ throw new Error('defineVariable() requires a string literal as the first parameter');
384
+ }
385
+
386
+ if (!varName) {
387
+ throw new Error('defineVariable() requires a string literal as the first parameter');
388
+ }
389
+
390
+ // Return a compiled expression that modifies the context
391
+ return {
392
+ fn: (ctx) => {
393
+ // Check if the variable is a system variable
394
+ const systemVariables = ['context', 'resource', 'rootResource', 'ucum', 'sct', 'loinc'];
395
+ if (systemVariables.includes(varName)) {
396
+ // Return empty result for system variable redefinition
397
+ return [];
398
+ }
399
+
400
+ // Check if the variable is already defined
401
+ // Since we're modifying ctx.env in place, we need to check if it exists
402
+ if (varName in ctx.env) {
403
+ // Return empty result for variable redefinition
404
+ return [];
405
+ }
406
+
407
+ // Evaluate the input expression
408
+ const inputVal = input.fn(ctx);
409
+
410
+ let value: any[];
411
+
412
+ if (valueExpr) {
413
+ // Create context for evaluating the value expression
414
+ // Set $this to the input value
415
+ const valueCtx = {
416
+ ...ctx,
417
+ input: inputVal,
418
+ focus: inputVal,
419
+ env: { ...ctx.env, $this: inputVal }
420
+ };
421
+
422
+ // Evaluate the value expression
423
+ value = valueExpr.fn(valueCtx);
424
+ } else {
425
+ // If no value expression is provided, use the input collection
426
+ value = inputVal;
427
+ }
428
+
429
+ // IMPORTANT: We need to modify the context object that was passed in
430
+ // so that subsequent operations can see the variable
431
+ // This is done by modifying the env object in place
432
+ ctx.env[varName] = value;
433
+
434
+ // Return the original input (not the value)
435
+ return inputVal;
436
+ },
437
+ type: input.type,
438
+ isSingleton: input.isSingleton,
439
+ source: valueExpr
440
+ ? `${input.source || ''}.defineVariable('${varName}', ${valueExpr.source || ''})`
441
+ : `${input.source || ''}.defineVariable('${varName}')`
442
+ };
443
+ }
444
+ };
445
+
446
+ export const traceFunction: Function = {
447
+ name: 'trace',
448
+ kind: 'function',
449
+
450
+ syntax: {
451
+ notation: 'trace(name, selector)'
452
+ },
453
+
454
+ signature: {
455
+ input: {
456
+ types: { kind: 'any' },
457
+ cardinality: 'collection'
458
+ },
459
+ parameters: [
460
+ {
461
+ name: 'name',
462
+ kind: 'value',
463
+ types: { kind: 'primitive', types: ['String'] },
464
+ cardinality: 'singleton',
465
+ optional: true
466
+ },
467
+ {
468
+ name: 'selector',
469
+ kind: 'expression',
470
+ types: { kind: 'any' },
471
+ cardinality: 'collection',
472
+ optional: true
473
+ }
474
+ ],
475
+ output: {
476
+ type: 'preserve-input',
477
+ cardinality: 'collection'
478
+ },
479
+ propagatesEmpty: false,
480
+ deterministic: false
481
+ },
482
+
483
+ analyze: defaultFunctionAnalyze,
484
+
485
+ evaluate: (interpreter, context, input, nameExpr, selectorExpr) => {
486
+ let values = input;
487
+
488
+ if (selectorExpr) {
489
+ const result = interpreter.evaluate(selectorExpr, input, context);
490
+ values = result.value;
491
+ }
492
+
493
+ console.log(`[TRACE] ${nameExpr || 'trace'}:`, values);
494
+
495
+ return { value: input, context };
496
+ },
497
+
498
+ compile: defaultFunctionCompile
499
+ };
500
+
501
+ export const checkFunction: Function = {
502
+ name: 'check',
503
+ kind: 'function',
504
+
505
+ syntax: {
506
+ notation: 'check(error, condition)'
507
+ },
508
+
509
+ signature: {
510
+ input: {
511
+ types: { kind: 'any' },
512
+ cardinality: 'collection'
513
+ },
514
+ parameters: [
515
+ {
516
+ name: 'error',
517
+ kind: 'expression',
518
+ types: { kind: 'any' },
519
+ cardinality: 'collection',
520
+ optional: false
521
+ },
522
+ {
523
+ name: 'condition',
524
+ kind: 'expression',
525
+ types: { kind: 'any' },
526
+ cardinality: 'collection',
527
+ optional: false
528
+ }
529
+ ],
530
+ output: {
531
+ type: 'preserve-input',
532
+ cardinality: 'collection'
533
+ },
534
+ propagatesEmpty: false,
535
+ deterministic: true
536
+ },
537
+
538
+ analyze: defaultFunctionAnalyze,
539
+
540
+ evaluate: (interpreter, context, input, errorExpr, conditionExpr) => {
541
+ const condResult = interpreter.evaluate(conditionExpr, input, context);
542
+
543
+ if (!isTruthy(condResult.value)) {
544
+ const errorResult = interpreter.evaluate(errorExpr, input, condResult.context);
545
+ const errorMessage = errorResult.value.join('');
546
+ throw new Error(`Check failed: ${errorMessage}`);
547
+ }
548
+
549
+ return { value: input, context: condResult.context };
550
+ },
551
+
552
+ compile: defaultFunctionCompile
553
+ };
@@ -0,0 +1,146 @@
1
+ import { TokenType } from '../lexer/token';
2
+ import type { Operation, Operator, Function, Literal } from './types';
3
+
4
+ export class Registry {
5
+ private static operations = new Map<string, Operation>();
6
+ private static tokenToOperation = new Map<TokenType, Operation>();
7
+ private static prefixOperators = new Map<TokenType, Operator>();
8
+ private static infixOperators = new Map<TokenType, Operator>();
9
+ private static postfixOperators = new Map<TokenType, Operator>();
10
+ private static precedenceTable = new Map<TokenType, number>();
11
+ private static literals: Literal[] = [];
12
+ private static keywords = new Set<string>();
13
+
14
+ static register(op: Operation) {
15
+ this.operations.set(op.name, op);
16
+
17
+ // Type-based registration
18
+ switch (op.kind) {
19
+ case 'operator':
20
+ // Register by form to handle operators with same token but different forms
21
+ if (op.syntax.form === 'prefix') {
22
+ this.prefixOperators.set(op.syntax.token, op);
23
+ } else if (op.syntax.form === 'infix') {
24
+ this.infixOperators.set(op.syntax.token, op);
25
+ } else if (op.syntax.form === 'postfix') {
26
+ this.postfixOperators.set(op.syntax.token, op);
27
+ }
28
+
29
+ // For backward compatibility, store in tokenToOperation (prioritize infix)
30
+ if (op.syntax.form === 'infix' || !this.tokenToOperation.has(op.syntax.token)) {
31
+ this.tokenToOperation.set(op.syntax.token, op);
32
+ }
33
+
34
+ // Only set precedence for infix operators in the precedence table
35
+ // Prefix and postfix operators have their own precedence but don't affect the infix precedence table
36
+ if (op.syntax.form === 'infix') {
37
+ this.precedenceTable.set(op.syntax.token, op.syntax.precedence);
38
+ }
39
+ // Register keyword operators (and, or, not, etc.)
40
+ if (/^[a-z]+$/.test(op.name)) {
41
+ this.keywords.add(op.name);
42
+ }
43
+ break;
44
+
45
+ case 'literal':
46
+ this.literals.push(op);
47
+ // Register keyword literals
48
+ if (op.syntax.keywords) {
49
+ op.syntax.keywords.forEach(kw => this.keywords.add(kw));
50
+ }
51
+ break;
52
+
53
+ case 'function':
54
+ // Functions don't need special registration
55
+ break;
56
+ }
57
+ }
58
+
59
+ static get(name: string): Operation | undefined {
60
+ return this.operations.get(name);
61
+ }
62
+
63
+ static getByToken(token: TokenType, form?: 'prefix' | 'infix' | 'postfix'): Operation | undefined {
64
+ if (form === 'prefix') {
65
+ return this.prefixOperators.get(token);
66
+ } else if (form === 'infix') {
67
+ return this.infixOperators.get(token);
68
+ } else if (form === 'postfix') {
69
+ return this.postfixOperators.get(token);
70
+ }
71
+ // Default fallback
72
+ return this.tokenToOperation.get(token);
73
+ }
74
+
75
+ static getPrecedence(token: TokenType): number {
76
+ return this.precedenceTable.get(token) ?? 0;
77
+ }
78
+
79
+ static isKeyword(word: string): boolean {
80
+ return this.keywords.has(word);
81
+ }
82
+
83
+ static getLiterals(): Literal[] {
84
+ return this.literals;
85
+ }
86
+
87
+ static matchLiteral(text: string): { operation: Literal; value: any } | null {
88
+ for (const literal of this.literals) {
89
+ if (literal.syntax.pattern && literal.syntax.pattern.test(text)) {
90
+ return {
91
+ operation: literal,
92
+ value: literal.parse(text)
93
+ };
94
+ }
95
+ if (literal.syntax.keywords && literal.syntax.keywords.includes(text)) {
96
+ return {
97
+ operation: literal,
98
+ value: literal.parse(text)
99
+ };
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // For special forms
106
+ static getSpecialForms(): Operator[] {
107
+ return Array.from(this.operations.values())
108
+ .filter((op): op is Operator => op.kind === 'operator' && op.syntax.special === true);
109
+ }
110
+
111
+ // Check if token starts a composite operator
112
+ static isCompositeOperatorStart(token: TokenType): boolean {
113
+ // Used by lexer to know when to look ahead
114
+ return [TokenType.LT, TokenType.GT, TokenType.NEQ, TokenType.NEQUIV]
115
+ .includes(token);
116
+ }
117
+
118
+ // Clear registry (useful for testing)
119
+ static clear() {
120
+ this.operations.clear();
121
+ this.tokenToOperation.clear();
122
+ this.prefixOperators.clear();
123
+ this.infixOperators.clear();
124
+ this.postfixOperators.clear();
125
+ this.precedenceTable.clear();
126
+ this.literals = [];
127
+ this.keywords.clear();
128
+ }
129
+
130
+ // Get all registered operations
131
+ static getAllOperations(): Operation[] {
132
+ return Array.from(this.operations.values());
133
+ }
134
+
135
+ // Get operators by form
136
+ static getOperatorsByForm(form: 'prefix' | 'infix' | 'postfix'): Operator[] {
137
+ return Array.from(this.operations.values())
138
+ .filter((op): op is Operator => op.kind === 'operator' && op.syntax.form === form);
139
+ }
140
+
141
+ // Get all functions
142
+ static getAllFunctions(): Function[] {
143
+ return Array.from(this.operations.values())
144
+ .filter((op): op is Function => op.kind === 'function');
145
+ }
146
+ }