@atomic-ehr/fhirpath 0.0.1-canary.1825db0.20250725140030

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 (59) hide show
  1. package/README.md +400 -0
  2. package/dist/index.d.ts +398 -0
  3. package/dist/index.js +8372 -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 +155 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +156 -0
  14. package/src/api/index.ts +70 -0
  15. package/src/api/inspect.ts +96 -0
  16. package/src/api/registry.ts +128 -0
  17. package/src/api/types.ts +210 -0
  18. package/src/compiler/compiler.ts +546 -0
  19. package/src/compiler/index.ts +2 -0
  20. package/src/compiler/prototype-context-adapter.ts +99 -0
  21. package/src/compiler/types.ts +24 -0
  22. package/src/index.ts +76 -0
  23. package/src/interpreter/README.md +78 -0
  24. package/src/interpreter/interpreter.ts +463 -0
  25. package/src/interpreter/types.ts +108 -0
  26. package/src/lexer/char-tables.ts +37 -0
  27. package/src/lexer/errors.ts +31 -0
  28. package/src/lexer/index.ts +5 -0
  29. package/src/lexer/lexer.ts +745 -0
  30. package/src/lexer/token.ts +104 -0
  31. package/src/parser/ast.ts +123 -0
  32. package/src/parser/index.ts +3 -0
  33. package/src/parser/parser.ts +701 -0
  34. package/src/parser/pprint.ts +169 -0
  35. package/src/registry/default-analyzers.ts +257 -0
  36. package/src/registry/default-compilers.ts +31 -0
  37. package/src/registry/index.ts +94 -0
  38. package/src/registry/operations/arithmetic.ts +506 -0
  39. package/src/registry/operations/collection.ts +425 -0
  40. package/src/registry/operations/comparison.ts +432 -0
  41. package/src/registry/operations/existence.ts +703 -0
  42. package/src/registry/operations/filtering.ts +358 -0
  43. package/src/registry/operations/literals.ts +341 -0
  44. package/src/registry/operations/logical.ts +439 -0
  45. package/src/registry/operations/math.ts +128 -0
  46. package/src/registry/operations/membership.ts +132 -0
  47. package/src/registry/operations/string.ts +507 -0
  48. package/src/registry/operations/subsetting.ts +174 -0
  49. package/src/registry/operations/type-checking.ts +162 -0
  50. package/src/registry/operations/type-conversion.ts +404 -0
  51. package/src/registry/operations/type-operators.ts +308 -0
  52. package/src/registry/operations/utility.ts +644 -0
  53. package/src/registry/registry.ts +146 -0
  54. package/src/registry/types.ts +161 -0
  55. package/src/registry/utils/evaluation-helpers.ts +93 -0
  56. package/src/registry/utils/index.ts +3 -0
  57. package/src/registry/utils/type-system.ts +173 -0
  58. package/src/runtime/context.ts +158 -0
  59. package/src/runtime/debug-context.ts +135 -0
@@ -0,0 +1,439 @@
1
+ import { TokenType } from '../../lexer/token';
2
+ import type { Operator, Function } from '../types';
3
+ import { defaultOperatorAnalyze, defaultFunctionAnalyze } from '../default-analyzers';
4
+ import { toBoolean, toSingleton } from '../utils';
5
+
6
+ export const andOperator: Operator = {
7
+ name: 'and',
8
+ kind: 'operator',
9
+
10
+ syntax: {
11
+ form: 'infix',
12
+ token: TokenType.AND,
13
+ precedence: 3,
14
+ associativity: 'left',
15
+ notation: 'a and b'
16
+ },
17
+
18
+ signature: {
19
+ parameters: [
20
+ { name: 'left', types: { kind: 'any' }, cardinality: 'any' },
21
+ { name: 'right', types: { kind: 'any' }, cardinality: 'any' }
22
+ ],
23
+ output: {
24
+ type: 'Boolean',
25
+ cardinality: 'singleton'
26
+ },
27
+ propagatesEmpty: false // Special three-valued logic
28
+ },
29
+
30
+ analyze: defaultOperatorAnalyze,
31
+
32
+ evaluate: (interpreter, context, input, left, right) => {
33
+ // Three-valued logic for and:
34
+ // true and true = true
35
+ // true and false = false
36
+ // true and empty = empty
37
+ // false and anything = false
38
+ // empty and true = empty
39
+ // empty and false = false
40
+ // empty and empty = empty
41
+
42
+ const leftEmpty = left.length === 0;
43
+ const rightEmpty = right.length === 0;
44
+
45
+ if (!leftEmpty) {
46
+ const leftBool = toBoolean(toSingleton(left));
47
+ if (!leftBool) {
48
+ // false and anything = false
49
+ return { value: [false], context };
50
+ }
51
+ // left is true
52
+ if (rightEmpty) {
53
+ // true and empty = empty
54
+ return { value: [], context };
55
+ }
56
+ // true and right
57
+ const rightBool = toBoolean(toSingleton(right));
58
+ return { value: [rightBool], context };
59
+ }
60
+
61
+ // left is empty
62
+ if (rightEmpty) {
63
+ // empty and empty = empty
64
+ return { value: [], context };
65
+ }
66
+
67
+ const rightBool = toBoolean(toSingleton(right));
68
+ if (!rightBool) {
69
+ // empty and false = false
70
+ return { value: [false], context };
71
+ }
72
+ // empty and true = empty
73
+ return { value: [], context };
74
+ },
75
+
76
+ compile: (compiler, input, args) => ({
77
+ fn: (ctx) => {
78
+ const left = args[0]?.fn(ctx) || [];
79
+ const right = args[1]?.fn(ctx) || [];
80
+
81
+ const leftEmpty = left.length === 0;
82
+ const rightEmpty = right.length === 0;
83
+
84
+ if (!leftEmpty) {
85
+ const leftBool = toBoolean(toSingleton(left));
86
+ if (!leftBool) return [false];
87
+ if (rightEmpty) return [];
88
+ return [toBoolean(toSingleton(right))];
89
+ }
90
+
91
+ if (rightEmpty) return [];
92
+ const rightBool = toBoolean(toSingleton(right));
93
+ if (!rightBool) return [false];
94
+ return [];
95
+ },
96
+ type: compiler.resolveType('Boolean'),
97
+ isSingleton: true,
98
+ source: `${args[0]?.source || ''} and ${args[1]?.source || ''}`
99
+ })
100
+ };
101
+
102
+ export const orOperator: Operator = {
103
+ name: 'or',
104
+ kind: 'operator',
105
+
106
+ syntax: {
107
+ form: 'infix',
108
+ token: TokenType.OR,
109
+ precedence: 2,
110
+ associativity: 'left',
111
+ notation: 'a or b'
112
+ },
113
+
114
+ signature: {
115
+ parameters: [
116
+ { name: 'left', types: { kind: 'any' }, cardinality: 'any' },
117
+ { name: 'right', types: { kind: 'any' }, cardinality: 'any' }
118
+ ],
119
+ output: {
120
+ type: 'Boolean',
121
+ cardinality: 'singleton'
122
+ },
123
+ propagatesEmpty: false // Special three-valued logic
124
+ },
125
+
126
+ analyze: defaultOperatorAnalyze,
127
+
128
+ evaluate: (interpreter, context, input, left, right) => {
129
+ // Three-valued logic for or:
130
+ // true or anything = true
131
+ // false or true = true
132
+ // false or false = false
133
+ // false or empty = empty
134
+ // empty or true = true
135
+ // empty or false = empty
136
+ // empty or empty = empty
137
+
138
+ const leftEmpty = left.length === 0;
139
+ const rightEmpty = right.length === 0;
140
+
141
+ if (!leftEmpty) {
142
+ const leftBool = toBoolean(toSingleton(left));
143
+ if (leftBool) {
144
+ // true or anything = true
145
+ return { value: [true], context };
146
+ }
147
+ // left is false
148
+ if (rightEmpty) {
149
+ // false or empty = empty
150
+ return { value: [], context };
151
+ }
152
+ // false or right
153
+ const rightBool = toBoolean(toSingleton(right));
154
+ return { value: [rightBool], context };
155
+ }
156
+
157
+ // left is empty
158
+ if (rightEmpty) {
159
+ // empty or empty = empty
160
+ return { value: [], context };
161
+ }
162
+
163
+ const rightBool = toBoolean(toSingleton(right));
164
+ if (rightBool) {
165
+ // empty or true = true
166
+ return { value: [true], context };
167
+ }
168
+ // empty or false = empty
169
+ return { value: [], context };
170
+ },
171
+
172
+ compile: (compiler, input, args) => ({
173
+ fn: (ctx) => {
174
+ const left = args[0]?.fn(ctx) || [];
175
+ const right = args[1]?.fn(ctx) || [];
176
+
177
+ const leftEmpty = left.length === 0;
178
+ const rightEmpty = right.length === 0;
179
+
180
+ if (!leftEmpty) {
181
+ const leftBool = toBoolean(toSingleton(left));
182
+ if (leftBool) return [true];
183
+ if (rightEmpty) return [];
184
+ return [toBoolean(toSingleton(right))];
185
+ }
186
+
187
+ if (rightEmpty) return [];
188
+ const rightBool = toBoolean(toSingleton(right));
189
+ if (rightBool) return [true];
190
+ return [];
191
+ },
192
+ type: compiler.resolveType('Boolean'),
193
+ isSingleton: true,
194
+ source: `${args[0]?.source || ''} or ${args[1]?.source || ''}`
195
+ })
196
+ };
197
+
198
+ export const notFunction: Function = {
199
+ name: 'not',
200
+ kind: 'function',
201
+
202
+ syntax: {
203
+ notation: 'not()'
204
+ },
205
+
206
+ signature: {
207
+ input: {
208
+ types: { kind: 'any' },
209
+ cardinality: 'any'
210
+ },
211
+ parameters: [], // not() takes no parameters
212
+ output: {
213
+ type: 'Boolean',
214
+ cardinality: 'singleton'
215
+ },
216
+ propagatesEmpty: false, // not() on empty returns true
217
+ deterministic: true
218
+ },
219
+
220
+ analyze: defaultFunctionAnalyze,
221
+
222
+ evaluate: (interpreter, context, input, ...args) => {
223
+ // not() should not accept any parameters
224
+ if (args.length > 0) {
225
+ throw new Error('not() function does not accept any parameters');
226
+ }
227
+
228
+ // not() behavior per spec:
229
+ // not(true) = false
230
+ // not(false) = true
231
+ // not(empty) = true
232
+ // not(single non-boolean) = false
233
+ // not(multiple values) = empty
234
+
235
+ if (input.length === 0) {
236
+ // not empty = true
237
+ return { value: [true], context };
238
+ }
239
+
240
+ if (input.length > 1) {
241
+ // not(multiple values) = empty
242
+ return { value: [], context };
243
+ }
244
+
245
+ // Single value - check if it's a boolean
246
+ const singleValue = input[0];
247
+ if (typeof singleValue === 'boolean') {
248
+ // Negate boolean value
249
+ return { value: [!singleValue], context };
250
+ }
251
+
252
+ // Non-boolean single value returns false
253
+ return { value: [false], context };
254
+ },
255
+
256
+ compile: (compiler, input) => ({
257
+ fn: (ctx) => {
258
+ const inputValue = input?.fn(ctx) || [];
259
+
260
+ // Match interpreter behavior
261
+ if (inputValue.length === 0) return [true];
262
+ if (inputValue.length > 1) return [];
263
+
264
+ const singleValue = inputValue[0];
265
+ if (typeof singleValue === 'boolean') {
266
+ return [!singleValue];
267
+ }
268
+
269
+ // Non-boolean single value returns false
270
+ return [false];
271
+ },
272
+ type: compiler.resolveType('Boolean'),
273
+ isSingleton: true,
274
+ source: `${input?.source || ''}.not()`
275
+ })
276
+ };
277
+
278
+ export const xorOperator: Operator = {
279
+ name: 'xor',
280
+ kind: 'operator',
281
+
282
+ syntax: {
283
+ form: 'infix',
284
+ token: TokenType.XOR,
285
+ precedence: 2,
286
+ associativity: 'left',
287
+ notation: 'a xor b'
288
+ },
289
+
290
+ signature: {
291
+ parameters: [
292
+ { name: 'left', types: { kind: 'any' }, cardinality: 'any' },
293
+ { name: 'right', types: { kind: 'any' }, cardinality: 'any' }
294
+ ],
295
+ output: {
296
+ type: 'Boolean',
297
+ cardinality: 'singleton'
298
+ },
299
+ propagatesEmpty: true
300
+ },
301
+
302
+ analyze: defaultOperatorAnalyze,
303
+
304
+ evaluate: (interpreter, context, input, left, right) => {
305
+ if (left.length === 0 || right.length === 0) return { value: [], context };
306
+
307
+ const leftBool = toBoolean(toSingleton(left));
308
+ const rightBool = toBoolean(toSingleton(right));
309
+
310
+ // XOR: true when exactly one is true
311
+ return { value: [leftBool !== rightBool], context };
312
+ },
313
+
314
+ compile: (compiler, input, args) => ({
315
+ fn: (ctx) => {
316
+ const left = args[0]?.fn(ctx) || [];
317
+ const right = args[1]?.fn(ctx) || [];
318
+
319
+ if (left.length === 0 || right.length === 0) return [];
320
+
321
+ const leftBool = toBoolean(toSingleton(left));
322
+ const rightBool = toBoolean(toSingleton(right));
323
+
324
+ return [leftBool !== rightBool];
325
+ },
326
+ type: compiler.resolveType('Boolean'),
327
+ isSingleton: true,
328
+ source: `${args[0]?.source || ''} xor ${args[1]?.source || ''}`
329
+ })
330
+ };
331
+
332
+ export const impliesOperator: Operator = {
333
+ name: 'implies',
334
+ kind: 'operator',
335
+
336
+ syntax: {
337
+ form: 'infix',
338
+ token: TokenType.IMPLIES,
339
+ precedence: 1, // Lowest precedence
340
+ associativity: 'left',
341
+ notation: 'a implies b'
342
+ },
343
+
344
+ signature: {
345
+ parameters: [
346
+ { name: 'left', types: { kind: 'any' }, cardinality: 'any' },
347
+ { name: 'right', types: { kind: 'any' }, cardinality: 'any' }
348
+ ],
349
+ output: {
350
+ type: 'Boolean',
351
+ cardinality: 'singleton'
352
+ },
353
+ propagatesEmpty: false // Special three-valued logic
354
+ },
355
+
356
+ analyze: defaultOperatorAnalyze,
357
+
358
+ evaluate: (interpreter, context, input, left, right) => {
359
+ // Implies truth table:
360
+ // true implies true = true
361
+ // true implies false = false
362
+ // true implies empty = empty
363
+ // false implies anything = true
364
+ // empty implies true = true
365
+ // empty implies false = empty
366
+ // empty implies empty = empty
367
+
368
+ const leftEmpty = left.length === 0;
369
+ const rightEmpty = right.length === 0;
370
+
371
+ if (!leftEmpty) {
372
+ const leftBool = toBoolean(toSingleton(left));
373
+ if (!leftBool) {
374
+ // false implies anything = true
375
+ return { value: [true], context };
376
+ }
377
+ // left is true
378
+ if (rightEmpty) {
379
+ // true implies empty = empty
380
+ return { value: [], context };
381
+ }
382
+ // true implies right
383
+ const rightBool = toBoolean(toSingleton(right));
384
+ return { value: [rightBool], context };
385
+ }
386
+
387
+ // left is empty
388
+ if (rightEmpty) {
389
+ // empty implies empty = empty
390
+ return { value: [], context };
391
+ }
392
+
393
+ const rightBool = toBoolean(toSingleton(right));
394
+ if (rightBool) {
395
+ // empty implies true = true
396
+ return { value: [true], context };
397
+ }
398
+ // empty implies false = empty
399
+ return { value: [], context };
400
+ },
401
+
402
+ compile: (compiler, input, args) => ({
403
+ fn: (ctx) => {
404
+ const left = args[0]?.fn(ctx) || [];
405
+ const right = args[1]?.fn(ctx) || [];
406
+
407
+ const leftEmpty = left.length === 0;
408
+ const rightEmpty = right.length === 0;
409
+
410
+ if (!leftEmpty) {
411
+ const leftBool = toBoolean(toSingleton(left));
412
+ if (!leftBool) return [true];
413
+ if (rightEmpty) return [];
414
+ return [toBoolean(toSingleton(right))];
415
+ }
416
+
417
+ if (rightEmpty) return [];
418
+ const rightBool = toBoolean(toSingleton(right));
419
+ if (rightBool) return [true];
420
+ return [];
421
+ },
422
+ type: compiler.resolveType('Boolean'),
423
+ isSingleton: true,
424
+ source: `${args[0]?.source || ''} implies ${args[1]?.source || ''}`
425
+ })
426
+ };
427
+
428
+ // Export all logical operators
429
+ export const logicalOperators = [
430
+ andOperator,
431
+ orOperator,
432
+ xorOperator,
433
+ impliesOperator
434
+ ];
435
+
436
+ // Export logical functions
437
+ export const logicalFunctions = [
438
+ notFunction
439
+ ];
@@ -0,0 +1,128 @@
1
+ import type { Function } from '../types';
2
+ import { defaultFunctionAnalyze } from '../default-analyzers';
3
+ import { defaultFunctionCompile } from '../default-compilers';
4
+ import { CollectionUtils } from '../../interpreter/types';
5
+
6
+ export const absFunction: Function = {
7
+ name: 'abs',
8
+ kind: 'function',
9
+
10
+ syntax: {
11
+ notation: 'abs()'
12
+ },
13
+
14
+ signature: {
15
+ input: {
16
+ types: { kind: 'union', types: ['Integer', 'Decimal', 'Quantity'] },
17
+ cardinality: 'singleton'
18
+ },
19
+ parameters: [],
20
+ output: {
21
+ type: 'preserve-input',
22
+ cardinality: 'singleton'
23
+ },
24
+ propagatesEmpty: true,
25
+ deterministic: true
26
+ },
27
+
28
+ analyze: defaultFunctionAnalyze,
29
+
30
+ evaluate: (interpreter, context, input) => {
31
+ if (input.length === 0) {
32
+ return { value: [], context };
33
+ }
34
+ const num = CollectionUtils.toSingleton(input);
35
+ return { value: [Math.abs(num)], context };
36
+ },
37
+
38
+ compile: defaultFunctionCompile
39
+ };
40
+
41
+ export const roundFunction: Function = {
42
+ name: 'round',
43
+ kind: 'function',
44
+
45
+ syntax: {
46
+ notation: 'round(precision)'
47
+ },
48
+
49
+ signature: {
50
+ input: {
51
+ types: { kind: 'union', types: ['Decimal'] },
52
+ cardinality: 'singleton'
53
+ },
54
+ parameters: [
55
+ {
56
+ name: 'precision',
57
+ kind: 'value',
58
+ types: { kind: 'primitive', types: ['Integer'] },
59
+ cardinality: 'singleton',
60
+ optional: true
61
+ }
62
+ ],
63
+ output: {
64
+ type: 'Decimal',
65
+ cardinality: 'singleton'
66
+ },
67
+ propagatesEmpty: true,
68
+ deterministic: true
69
+ },
70
+
71
+ analyze: defaultFunctionAnalyze,
72
+
73
+ evaluate: (interpreter, context, input, precision) => {
74
+ if (input.length === 0) {
75
+ return { value: [], context };
76
+ }
77
+ const num = CollectionUtils.toSingleton(input);
78
+
79
+ if (precision === undefined) {
80
+ return { value: [Math.round(num)], context };
81
+ }
82
+
83
+ const factor = Math.pow(10, precision);
84
+ return { value: [Math.round(num * factor) / factor], context };
85
+ },
86
+
87
+ compile: defaultFunctionCompile
88
+ };
89
+
90
+ export const sqrtFunction: Function = {
91
+ name: 'sqrt',
92
+ kind: 'function',
93
+
94
+ syntax: {
95
+ notation: 'sqrt()'
96
+ },
97
+
98
+ signature: {
99
+ input: {
100
+ types: { kind: 'union', types: ['Decimal'] },
101
+ cardinality: 'singleton'
102
+ },
103
+ parameters: [],
104
+ output: {
105
+ type: 'Decimal',
106
+ cardinality: 'singleton'
107
+ },
108
+ propagatesEmpty: true,
109
+ deterministic: true
110
+ },
111
+
112
+ analyze: defaultFunctionAnalyze,
113
+
114
+ evaluate: (interpreter, context, input) => {
115
+ if (input.length === 0) {
116
+ return { value: [], context };
117
+ }
118
+ const num = CollectionUtils.toSingleton(input);
119
+
120
+ if (num < 0) {
121
+ return { value: [], context };
122
+ }
123
+
124
+ return { value: [Math.sqrt(num)], context };
125
+ },
126
+
127
+ compile: defaultFunctionCompile
128
+ };
@@ -0,0 +1,132 @@
1
+ import { TokenType } from '../../lexer/token';
2
+ import type { Operator } from '../types';
3
+ import { defaultOperatorAnalyze } from '../default-analyzers';
4
+ import { defaultOperatorCompile } from '../default-compilers';
5
+
6
+ // Helper function to check if value is in collection
7
+ function isIn(value: any, collection: any[]): boolean {
8
+ return collection.some(item => {
9
+ if (value === item) return true;
10
+ if (value == null || item == null) return false;
11
+
12
+ // Handle complex equality
13
+ if (typeof value === 'object' && typeof item === 'object') {
14
+ return JSON.stringify(value) === JSON.stringify(item);
15
+ }
16
+
17
+ return false;
18
+ });
19
+ }
20
+
21
+
22
+ export const inOperator: Operator = {
23
+ name: 'in',
24
+ kind: 'operator',
25
+ syntax: {
26
+ form: 'infix',
27
+ token: TokenType.IN,
28
+ precedence: 10,
29
+ associativity: 'left',
30
+ notation: 'a in b'
31
+ },
32
+ signature: {
33
+ parameters: [{ name: 'left' }, { name: 'right' }],
34
+ propagatesEmpty: false,
35
+ output: {
36
+ type: 'Boolean',
37
+ cardinality: 'singleton'
38
+ }
39
+ },
40
+ analyze: defaultOperatorAnalyze,
41
+ evaluate: (_interpreter, context, _input, element, collection) => {
42
+ if (element.length === 0 || collection.length === 0) return { value: [], context };
43
+
44
+ // Handle string substring check
45
+ if (collection.length === 1 && typeof collection[0] === 'string' && typeof element[0] === 'string') {
46
+ return { value: [collection[0].includes(element[0])], context };
47
+ }
48
+
49
+ return { value: [isIn(element[0], collection)], context };
50
+ },
51
+ compile: (compiler, _input, args) => {
52
+ const [leftExpr, rightExpr] = args;
53
+ if (!leftExpr || !rightExpr) {
54
+ throw new Error('in operator requires two arguments');
55
+ }
56
+ return {
57
+ fn: (ctx) => {
58
+ const left = leftExpr.fn(ctx);
59
+ const right = rightExpr.fn(ctx);
60
+ if (left.length === 0 || right.length === 0) return [];
61
+
62
+ // Handle string substring check
63
+ if (right.length === 1 && typeof right[0] === 'string' && typeof left[0] === 'string') {
64
+ return [right[0].includes(left[0])];
65
+ }
66
+
67
+ return [isIn(left[0], right)];
68
+ },
69
+ type: compiler.resolveType('Boolean'),
70
+ isSingleton: true
71
+ };
72
+ }
73
+ };
74
+
75
+ export const containsOperator: Operator = {
76
+ name: 'contains',
77
+ kind: 'operator',
78
+ syntax: {
79
+ form: 'infix',
80
+ token: TokenType.CONTAINS,
81
+ precedence: 10,
82
+ associativity: 'left',
83
+ notation: 'a contains b'
84
+ },
85
+ signature: {
86
+ parameters: [{ name: 'left' }, { name: 'right' }],
87
+ propagatesEmpty: false,
88
+ output: {
89
+ type: 'Boolean',
90
+ cardinality: 'singleton'
91
+ }
92
+ },
93
+ analyze: defaultOperatorAnalyze,
94
+ evaluate: (_interpreter, context, _input, collection, element) => {
95
+ if (collection.length === 0 || element.length === 0) return { value: [], context };
96
+
97
+ // Handle string substring check
98
+ if (collection.length === 1 && typeof collection[0] === 'string' && typeof element[0] === 'string') {
99
+ return { value: [collection[0].includes(element[0])], context };
100
+ }
101
+
102
+ return { value: [isIn(element[0], collection)], context };
103
+ },
104
+ compile: (compiler, _input, args) => {
105
+ const [leftExpr, rightExpr] = args;
106
+ if (!leftExpr || !rightExpr) {
107
+ throw new Error('contains operator requires two arguments');
108
+ }
109
+ return {
110
+ fn: (ctx) => {
111
+ const left = leftExpr.fn(ctx);
112
+ const right = rightExpr.fn(ctx);
113
+ if (left.length === 0 || right.length === 0) return [];
114
+
115
+ // Handle string substring check
116
+ if (left.length === 1 && typeof left[0] === 'string' && typeof right[0] === 'string') {
117
+ return [left[0].includes(right[0])];
118
+ }
119
+
120
+ return [isIn(right[0], left)];
121
+ },
122
+ type: compiler.resolveType('Boolean'),
123
+ isSingleton: true
124
+ };
125
+ }
126
+ };
127
+
128
+ // Export membership operators
129
+ export const membershipOperators = [
130
+ inOperator,
131
+ containsOperator
132
+ ];