@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.
- package/README.md +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8185 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +152 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
- 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
|
+
}
|