@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800

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