@routier/core 0.6.0 → 0.7.0

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 (52) hide show
  1. package/dist/assertions/index.cjs +19 -8
  2. package/dist/assertions/index.cjs.map +1 -1
  3. package/dist/assertions/index.d.ts +5 -1
  4. package/dist/assertions/index.js +21 -9
  5. package/dist/assertions/index.js.map +1 -1
  6. package/dist/collections/MemoryDataCollection.d.ts +10 -0
  7. package/dist/collections/index.cjs +29 -4
  8. package/dist/collections/index.cjs.map +1 -1
  9. package/dist/collections/index.js +29 -4
  10. package/dist/collections/index.js.map +1 -1
  11. package/dist/expressions/callSource.d.ts +41 -0
  12. package/dist/expressions/evaluate.d.ts +3 -0
  13. package/dist/expressions/fold.d.ts +7 -0
  14. package/dist/expressions/index.cjs +1754 -233
  15. package/dist/expressions/index.cjs.map +1 -1
  16. package/dist/expressions/index.d.ts +2 -0
  17. package/dist/expressions/index.js +1765 -234
  18. package/dist/expressions/index.js.map +1 -1
  19. package/dist/expressions/types.d.ts +45 -26
  20. package/dist/expressions/utils.d.ts +19 -1
  21. package/dist/index.cjs +2415 -364
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +2755 -684
  24. package/dist/index.js.map +1 -1
  25. package/dist/performance/index.cjs +6 -4
  26. package/dist/performance/index.cjs.map +1 -1
  27. package/dist/performance/index.js +6 -4
  28. package/dist/performance/index.js.map +1 -1
  29. package/dist/pipeline/index.cjs +6 -4
  30. package/dist/pipeline/index.cjs.map +1 -1
  31. package/dist/pipeline/index.js +6 -4
  32. package/dist/pipeline/index.js.map +1 -1
  33. package/dist/plugins/index.cjs +2309 -316
  34. package/dist/plugins/index.cjs.map +1 -1
  35. package/dist/plugins/index.js +2313 -311
  36. package/dist/plugins/index.js.map +1 -1
  37. package/dist/plugins/query/QueryOptionsCollection.d.ts +38 -10
  38. package/dist/plugins/query/describeFilter.d.ts +83 -0
  39. package/dist/plugins/query/explain.d.ts +71 -9
  40. package/dist/plugins/query/index.d.ts +1 -0
  41. package/dist/plugins/query/join.d.ts +4 -1
  42. package/dist/plugins/query/types.d.ts +36 -4
  43. package/dist/schema/PropertyInfo.d.ts +0 -1
  44. package/dist/schema/index.cjs +7 -14
  45. package/dist/schema/index.cjs.map +1 -1
  46. package/dist/schema/index.js +7 -14
  47. package/dist/schema/index.js.map +1 -1
  48. package/dist/utilities/index.cjs +242 -49
  49. package/dist/utilities/index.cjs.map +1 -1
  50. package/dist/utilities/index.js +242 -49
  51. package/dist/utilities/index.js.map +1 -1
  52. package/package.json +1 -1
@@ -4,6 +4,7 @@ var __webpack_modules__ = ({
4
4
  126(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
5
5
  __webpack_require__.d(__webpack_exports__, {
6
6
  assertString: () => (assertString),
7
+ isCallExpression: () => (isCallExpression),
7
8
  isComparatorExpression: () => (isComparatorExpression),
8
9
  isOperatorExpression: () => (isOperatorExpression),
9
10
  isPropertyExpression: () => (isPropertyExpression),
@@ -79,6 +80,11 @@ function isObjectWithType(value) {
79
80
  */ function isValueExpression(value) {
80
81
  return isObjectWithType(value) && value.type === "value";
81
82
  }
83
+ /**
84
+ * Type guard: narrows `value` to `CallExpression` when it is an object with `type === "call"`.
85
+ */ function isCallExpression(value) {
86
+ return isObjectWithType(value) && value.type === "call";
87
+ }
82
88
  /**
83
89
  * Type guard: narrows `value` to `EmptyExpression` when it is an object with `type === "empty"`.
84
90
  */ function isEmptyExpression(value) {
@@ -91,62 +97,396 @@ function isObjectWithType(value) {
91
97
  }
92
98
 
93
99
 
100
+ },
101
+ 429(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
102
+ __webpack_require__.d(__webpack_exports__, {
103
+ N: () => (CALL_SOURCE),
104
+ a: () => (renderCallAsJs)
105
+ });
106
+ const CALL_SOURCE = {
107
+ "to-lower-case": {
108
+ form: "method",
109
+ name: "toLowerCase"
110
+ },
111
+ "to-upper-case": {
112
+ form: "method",
113
+ name: "toUpperCase"
114
+ },
115
+ "length": {
116
+ form: "property",
117
+ name: "length"
118
+ },
119
+ "trim": {
120
+ form: "method",
121
+ name: "trim"
122
+ },
123
+ "trim-start": {
124
+ form: "method",
125
+ name: "trimStart"
126
+ },
127
+ "trim-end": {
128
+ form: "method",
129
+ name: "trimEnd"
130
+ },
131
+ "index-of": {
132
+ form: "method",
133
+ name: "indexOf"
134
+ },
135
+ "substring": {
136
+ form: "method",
137
+ name: "substring"
138
+ },
139
+ "concat": {
140
+ form: "method",
141
+ name: "concat"
142
+ },
143
+ "replace": {
144
+ form: "method",
145
+ name: "replace"
146
+ },
147
+ "replace-all": {
148
+ form: "method",
149
+ name: "replaceAll"
150
+ },
151
+ "absolute": {
152
+ form: "function",
153
+ name: "Math.abs"
154
+ },
155
+ "floor": {
156
+ form: "function",
157
+ name: "Math.floor"
158
+ },
159
+ "ceiling": {
160
+ form: "function",
161
+ name: "Math.ceil"
162
+ },
163
+ "round": {
164
+ form: "function",
165
+ name: "Math.round"
166
+ },
167
+ "sign": {
168
+ form: "function",
169
+ name: "Math.sign"
170
+ },
171
+ "square-root": {
172
+ form: "function",
173
+ name: "Math.sqrt"
174
+ },
175
+ "add": {
176
+ form: "operator",
177
+ symbol: "+"
178
+ },
179
+ "subtract": {
180
+ form: "operator",
181
+ symbol: "-"
182
+ },
183
+ "multiply": {
184
+ form: "operator",
185
+ symbol: "*"
186
+ },
187
+ "divide": {
188
+ form: "operator",
189
+ symbol: "/"
190
+ },
191
+ "modulo": {
192
+ form: "operator",
193
+ symbol: "%"
194
+ },
195
+ "utc-year": {
196
+ form: "method",
197
+ name: "getUTCFullYear"
198
+ },
199
+ "utc-month": {
200
+ form: "method",
201
+ name: "getUTCMonth"
202
+ },
203
+ "utc-day-of-month": {
204
+ form: "method",
205
+ name: "getUTCDate"
206
+ },
207
+ "utc-day-of-week": {
208
+ form: "method",
209
+ name: "getUTCDay"
210
+ },
211
+ "utc-hour": {
212
+ form: "method",
213
+ name: "getUTCHours"
214
+ },
215
+ "utc-minute": {
216
+ form: "method",
217
+ name: "getUTCMinutes"
218
+ },
219
+ "utc-second": {
220
+ form: "method",
221
+ name: "getUTCSeconds"
222
+ },
223
+ "utc-millisecond": {
224
+ form: "method",
225
+ name: "getUTCMilliseconds"
226
+ },
227
+ "epoch-ms": {
228
+ form: "method",
229
+ name: "getTime"
230
+ },
231
+ "to-string": {
232
+ form: "function",
233
+ name: "String"
234
+ },
235
+ "to-number": {
236
+ form: "function",
237
+ name: "Number"
238
+ },
239
+ "to-boolean": {
240
+ form: "function",
241
+ name: "Boolean"
242
+ },
243
+ "type-of": {
244
+ form: "prefix",
245
+ keyword: "typeof"
246
+ },
247
+ "some": {
248
+ form: "method",
249
+ name: "some"
250
+ },
251
+ "every": {
252
+ form: "method",
253
+ name: "every"
254
+ },
255
+ // `Math.pow(a, b)` parses to the same call; `**` is the shorter of the two spellings
256
+ "power": {
257
+ form: "operator",
258
+ symbol: "**"
259
+ },
260
+ "bit-and": {
261
+ form: "operator",
262
+ symbol: "&"
263
+ },
264
+ "bit-or": {
265
+ form: "operator",
266
+ symbol: "|"
267
+ },
268
+ "bit-xor": {
269
+ form: "operator",
270
+ symbol: "^"
271
+ },
272
+ "shift-left": {
273
+ form: "operator",
274
+ symbol: "<<"
275
+ },
276
+ "shift-right": {
277
+ form: "operator",
278
+ symbol: ">>"
279
+ },
280
+ "shift-right-unsigned": {
281
+ form: "operator",
282
+ symbol: ">>>"
283
+ },
284
+ "bit-not": {
285
+ form: "prefix",
286
+ keyword: "~"
287
+ },
288
+ "coalesce": {
289
+ form: "operator",
290
+ symbol: "??"
291
+ },
292
+ "conditional": {
293
+ form: "conditional"
294
+ },
295
+ "matches": {
296
+ form: "regex-test"
297
+ }
298
+ };
299
+ /**
300
+ * A call rendered as the JavaScript that produced it, from operand and argument text already
301
+ * rendered by the caller.
302
+ *
303
+ * Takes strings so one implementation serves a live tree and a serialized one.
304
+ */ /**
305
+ * Thunked because rendering a side can record a parameter, and `regex-test` emits its argument
306
+ * before its operand — so the two orders have to agree.
307
+ */ const renderCallAsJs = (call, renderOperand, renderArgs)=>{
308
+ const source = CALL_SOURCE[call];
309
+ if (source == null) {
310
+ const operand = renderOperand();
311
+ return `${operand}.${call}(${renderArgs().join(", ")})`;
312
+ }
313
+ if (source.form === "property") {
314
+ return `${renderOperand()}.${source.name}`;
315
+ }
316
+ if (source.form === "regex-test") {
317
+ const pattern = renderArgs()[0] ?? "?";
318
+ return `${pattern}.test(${renderOperand()})`;
319
+ }
320
+ if (source.form === "method") {
321
+ const operand = renderOperand();
322
+ return `${operand}.${source.name}(${renderArgs().join(", ")})`;
323
+ }
324
+ if (source.form === "function") {
325
+ const operand = renderOperand();
326
+ return `${source.name}(${[
327
+ operand,
328
+ ...renderArgs()
329
+ ].join(", ")})`;
330
+ }
331
+ if (source.form === "prefix") {
332
+ // `~x`, not `~ x` — a bitwise complement is written tight, unlike `typeof`
333
+ const operand = renderOperand();
334
+ return source.keyword === "~" ? `${source.keyword}${operand}` : `${source.keyword} ${operand}`;
335
+ }
336
+ if (source.form === "conditional") {
337
+ const operand = renderOperand();
338
+ const args = renderArgs();
339
+ return `${operand} ? ${args[0] ?? "?"} : ${args[1] ?? "?"}`;
340
+ }
341
+ const operand = renderOperand();
342
+ return `${[
343
+ operand,
344
+ ...renderArgs()
345
+ ].join(` ${source.symbol} `)}`;
346
+ };
347
+
348
+
94
349
  },
95
350
  835(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
96
351
  __webpack_require__.d(__webpack_exports__, {
97
352
  t: () => (EXPRESSION_TYPES)
98
353
  });
99
- const EXPRESSION_TYPES = [
100
- "operator",
101
- "comparator",
102
- "property",
103
- "value",
104
- "empty",
105
- "not-parsable"
106
- ];
354
+ /**
355
+ * A `Record` rather than a list, so adding to `ExpressionType` without adding it here is a compile
356
+ * error. As a list it was not exhaustive, and `call` was silently missing from `isExpression`.
357
+ */ const EXPRESSION_TYPE_SET = {
358
+ "operator": true,
359
+ "comparator": true,
360
+ "property": true,
361
+ "value": true,
362
+ "call": true,
363
+ "empty": true,
364
+ "not-parsable": true
365
+ };
366
+ const EXPRESSION_TYPES = Object.keys(EXPRESSION_TYPE_SET);
107
367
 
108
368
 
109
369
  },
110
370
  379(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
111
371
  __webpack_require__.d(__webpack_exports__, {
112
372
  Vu: () => (toPredicate),
373
+ Vv: () => (operandValue),
113
374
  _3: () => (evaluate),
375
+ gm: () => (UNRESOLVED),
114
376
  wS: () => (toStrictPredicate)
115
377
  });
116
378
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
117
379
 
118
380
  /** Reads a property or literal operand, or `UNRESOLVED` when the node is not one. */ const UNRESOLVED = Symbol("unresolved");
119
- const applyTransformer = (value, transformer)=>{
120
- if (transformer == null) {
121
- return value;
122
- }
123
- // A transformer applied to an absent value has no answer, and inventing one ("" for a missing
124
- // string) is how a filter starts matching rows it should not.
381
+ const ARITHMETIC = {
382
+ "add": (left, right)=>left + right,
383
+ "subtract": (left, right)=>left - right,
384
+ "multiply": (left, right)=>left * right,
385
+ "divide": (left, right)=>left / right,
386
+ "modulo": (left, right)=>left % right,
387
+ "power": (left, right)=>left ** right,
388
+ "bit-and": (left, right)=>left & right,
389
+ "bit-or": (left, right)=>left | right,
390
+ "bit-xor": (left, right)=>left ^ right,
391
+ "shift-left": (left, right)=>left << right,
392
+ "shift-right": (left, right)=>left >> right,
393
+ "shift-right-unsigned": (left, right)=>left >>> right
394
+ };
395
+ const applyCall = (call, value, args)=>{
396
+ // Above the guard: a template renders null as "null" in JavaScript, so these two are total.
397
+ if (call === "to-string") {
398
+ return String(value);
399
+ }
400
+ if (call === "concat") {
401
+ return [
402
+ value,
403
+ ...args
404
+ ].map(String).join("");
405
+ }
406
+ // A call applied to an absent value has no answer, and inventing one ("" for a missing string)
407
+ // is how a filter starts matching rows it should not.
125
408
  if (value == null) {
126
409
  return UNRESOLVED;
127
410
  }
128
- if (transformer === "to-lower-case") {
129
- return typeof value === "string" ? value.toLowerCase() : UNRESOLVED;
130
- }
131
- if (transformer === "to-upper-case") {
132
- return typeof value === "string" ? value.toUpperCase() : UNRESOLVED;
411
+ if (call === "to-lower-case" || call === "to-upper-case") {
412
+ if (typeof value !== "string") {
413
+ return UNRESOLVED;
414
+ }
415
+ const lower = call === "to-lower-case";
416
+ if (args.length === 0 || args[0] == null) {
417
+ return lower ? value.toLowerCase() : value.toUpperCase();
418
+ }
419
+ if (typeof args[0] !== "string") {
420
+ return UNRESOLVED;
421
+ }
422
+ try {
423
+ // An explicit locale is deterministic; dropping it answers a different question in Turkish.
424
+ return lower ? value.toLocaleLowerCase(args[0]) : value.toLocaleUpperCase(args[0]);
425
+ } catch {
426
+ // An invalid language tag throws RangeError; no answer beats the host's default.
427
+ return UNRESOLVED;
428
+ }
133
429
  }
134
- if (transformer === "length") {
430
+ if (call === "length") {
135
431
  return typeof value === "string" || Array.isArray(value) ? value.length : UNRESOLVED;
136
432
  }
433
+ if (call === "bit-not") {
434
+ return typeof value === "number" ? ~value : UNRESOLVED;
435
+ }
436
+ if (call === "matches") {
437
+ if (typeof value !== "string" || !(args[0] instanceof RegExp)) {
438
+ return UNRESOLVED;
439
+ }
440
+ // `test` advances `lastIndex` on a global or sticky pattern, and the pattern is shared with
441
+ // the cached template, where a source evaluates fresh in JavaScript.
442
+ return args[0].global || args[0].sticky ? new RegExp(args[0].source, args[0].flags).test(value) : args[0].test(value);
443
+ }
444
+ const arithmetic = ARITHMETIC[call];
445
+ if (arithmetic != null) {
446
+ return typeof value === "number" && typeof args[0] === "number" ? arithmetic(value, args[0]) : UNRESOLVED;
447
+ }
137
448
  return UNRESOLVED;
138
449
  };
139
- const operand = (expression, row)=>{
450
+ const operandValue = (expression, row)=>{
140
451
  if (expression == null) {
141
452
  return UNRESOLVED;
142
453
  }
143
454
  if ((0,_assertions__rspack_import_0.isValueExpression)(expression)) {
144
- return applyTransformer(expression.value, expression.transformer);
455
+ return expression.value;
145
456
  }
146
457
  if ((0,_assertions__rspack_import_0.isPropertyExpression)(expression)) {
147
458
  // Through the PropertyInfo, so a nested path and a `from`-renamed segment resolve the same
148
459
  // way every other consumer of the tree resolves them.
149
- return applyTransformer(expression.property.getValue(row), expression.transformer);
460
+ return expression.property.getValue(row);
461
+ }
462
+ if ((0,_assertions__rspack_import_0.isCallExpression)(expression)) {
463
+ /**
464
+ * `??` and `? :` are the two calls whose whole job is to answer when something is absent, so
465
+ * they run before the guard that refuses an absent operand.
466
+ */ if (expression.call === "coalesce") {
467
+ const left = operandValue(expression.expression, row);
468
+ return left === UNRESOLVED || left == null ? operandValue(expression.arguments[0], row) : left;
469
+ }
470
+ if (expression.call === "conditional") {
471
+ const condition = evaluate(expression.expression, row);
472
+ if (condition === undefined) {
473
+ return UNRESOLVED;
474
+ }
475
+ return operandValue(expression.arguments[condition === true ? 0 : 1], row);
476
+ }
477
+ const inner = operandValue(expression.expression, row);
478
+ if (inner === UNRESOLVED) {
479
+ return UNRESOLVED;
480
+ }
481
+ const args = [];
482
+ for (const argument of expression.arguments){
483
+ const resolved = operandValue(argument, row);
484
+ if (resolved === UNRESOLVED) {
485
+ return UNRESOLVED;
486
+ }
487
+ args.push(resolved);
488
+ }
489
+ return applyCall(expression.call, inner, args);
150
490
  }
151
491
  return UNRESOLVED;
152
492
  };
@@ -231,8 +571,8 @@ const evaluateComparator = (comparator, left, right, strict)=>{
231
571
  return left === false && right === false ? false : undefined;
232
572
  }
233
573
  if ((0,_assertions__rspack_import_0.isComparatorExpression)(expression)) {
234
- const left = operand(expression.left, row);
235
- const right = operand(expression.right, row);
574
+ const left = operandValue(expression.left, row);
575
+ const right = operandValue(expression.right, row);
236
576
  if (left === UNRESOLVED || right === UNRESOLVED) {
237
577
  return undefined;
238
578
  }
@@ -270,6 +610,114 @@ const evaluateComparator = (comparator, left, right, strict)=>{
270
610
  };
271
611
 
272
612
 
613
+ },
614
+ 43(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
615
+ __webpack_require__.d(__webpack_exports__, {
616
+ F5: () => (foldConstantCalls),
617
+ Sv: () => (FOLDABLE),
618
+ br: () => (foldedOperandValue)
619
+ });
620
+ /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
621
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
622
+ /* import */ var _types__rspack_import_2 = __webpack_require__(27);
623
+ /* import */ var _utils__rspack_import_1 = __webpack_require__(63);
624
+
625
+
626
+
627
+
628
+ /** Calls fold may compute. Absent means a plugin declines it, so a new call is opt-in. */ const FOLDABLE = new Set([
629
+ "to-lower-case",
630
+ "to-upper-case",
631
+ "length",
632
+ "bit-not",
633
+ "matches",
634
+ "to-string",
635
+ "concat",
636
+ "add",
637
+ "subtract",
638
+ "multiply",
639
+ "divide",
640
+ "modulo",
641
+ "power",
642
+ "bit-and",
643
+ "bit-or",
644
+ "bit-xor",
645
+ "shift-left",
646
+ "shift-right",
647
+ "shift-right-unsigned",
648
+ "coalesce",
649
+ "conditional"
650
+ ]);
651
+ /** `String(value)` on an object is the host's rendering — a Date carries its timezone. */ const COERCES_TO_TEXT = new Set([
652
+ "to-string",
653
+ "concat"
654
+ ]);
655
+ const isFrozenPrimitive = (value)=>value == null || typeof value !== "object";
656
+ const readsAProperty = (expression)=>{
657
+ if ((0,_assertions__rspack_import_0.isPropertyExpression)(expression)) {
658
+ return true;
659
+ }
660
+ return (0,_utils__rspack_import_1/* .childrenOf */.LU)(expression).some(readsAProperty);
661
+ };
662
+ /** A `conditional` holds a condition where every other call holds a value. */ const isConstant = (call)=>{
663
+ if (!FOLDABLE.has(call.call) || !call.arguments.every(_assertions__rspack_import_0.isValueExpression)) {
664
+ return false;
665
+ }
666
+ if (COERCES_TO_TEXT.has(call.call) && [
667
+ call.expression,
668
+ ...call.arguments
669
+ ].some((operand)=>(0,_assertions__rspack_import_0.isValueExpression)(operand) && !isFrozenPrimitive(operand.value))) {
670
+ return false;
671
+ }
672
+ return call.call === "conditional" ? !readsAProperty(call.expression) : (0,_assertions__rspack_import_0.isValueExpression)(call.expression);
673
+ };
674
+ /** Computes every call whose operand and arguments are all literals. Runs after `bindExpression`. */ const foldConstantCalls = (expression)=>{
675
+ if ((0,_assertions__rspack_import_0.isCallExpression)(expression)) {
676
+ const folded = new _types__rspack_import_2/* .CallExpression */.DG({
677
+ call: expression.call,
678
+ expression: foldConstantCalls(expression.expression),
679
+ arguments: expression.arguments.map(foldConstantCalls)
680
+ });
681
+ if (!isConstant(folded)) {
682
+ return folded;
683
+ }
684
+ const value = (0,_evaluate__rspack_import_3/* .operandValue */.Vv)(folded, {});
685
+ return value === _evaluate__rspack_import_3/* .UNRESOLVED */.gm ? folded : new _types__rspack_import_2/* .ValueExpression */.Ko({
686
+ value
687
+ });
688
+ }
689
+ if ((0,_assertions__rspack_import_0.isComparatorExpression)(expression)) {
690
+ return new _types__rspack_import_2/* .ComparatorExpression */.bQ({
691
+ comparator: expression.comparator,
692
+ negated: expression.negated,
693
+ strict: expression.strict,
694
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
695
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
696
+ });
697
+ }
698
+ if ((0,_assertions__rspack_import_0.isOperatorExpression)(expression)) {
699
+ return new _types__rspack_import_2/* .OperatorExpression */.fw({
700
+ operator: expression.operator,
701
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
702
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
703
+ });
704
+ }
705
+ return expression;
706
+ };
707
+ /** The value a literal operand binds as once the calls on it are computed. Throws if it cannot. */ const foldedOperandValue = (operand, calls)=>{
708
+ if (calls.length === 0) {
709
+ return operand.value;
710
+ }
711
+ // `peelCalls` returns calls innermost first, so the last one evaluates the whole chain.
712
+ const outermost = calls[calls.length - 1];
713
+ const value = readsAProperty(outermost) ? _evaluate__rspack_import_3/* .UNRESOLVED */.gm : (0,_evaluate__rspack_import_3/* .operandValue */.Vv)(outermost, {});
714
+ if (value === _evaluate__rspack_import_3/* .UNRESOLVED */.gm) {
715
+ throw new Error(`'${calls.map((call)=>call.call).join("', '")}' cannot be computed on the literal ` + `'${String(operand.value)}'.`);
716
+ }
717
+ return value;
718
+ };
719
+
720
+
273
721
  },
274
722
  91(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
275
723
  __webpack_require__.d(__webpack_exports__, {
@@ -277,14 +725,18 @@ __webpack_require__.d(__webpack_exports__, {
277
725
  oH: () => (parseFragment),
278
726
  pg: () => (combineExpressions)
279
727
  });
280
- /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
728
+ /* import */ var _utilities__rspack_import_5 = __webpack_require__(581);
281
729
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
282
730
  /* import */ var _schema__rspack_import_2 = __webpack_require__(537);
731
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
732
+ /* import */ var _fold__rspack_import_4 = __webpack_require__(43);
283
733
  /* import */ var _types__rspack_import_1 = __webpack_require__(27);
284
734
 
285
735
 
286
736
 
287
737
 
738
+
739
+
288
740
  // Error message constants
289
741
  const ERROR_MESSAGES = {
290
742
  PROPERTY_NOT_FOUND: (path)=>`Error parsing query, could not find PropertyInfo for path: ${path}`,
@@ -330,8 +782,13 @@ const converters = {
330
782
  };
331
783
  // Longest first so multi-character punctuation wins over its prefixes
332
784
  const MULTI_CHARACTER_PUNCTUATION = [
785
+ ">>>",
333
786
  "===",
334
787
  "!==",
788
+ "**",
789
+ "<<",
790
+ ">>",
791
+ "??",
335
792
  "?.",
336
793
  "&&",
337
794
  "||",
@@ -368,9 +825,17 @@ const SINGLE_CHARACTER_PUNCTUATION = new Set([
368
825
  "?",
369
826
  ":",
370
827
  "&",
371
- "|"
828
+ "|",
829
+ "^",
830
+ "~"
372
831
  ]);
373
- const STRING_ESCAPES = {
832
+ /**
833
+ * A lookup table keyed by source text.
834
+ *
835
+ * Null-prototype: `TRANSFORM_METHODS["toString"]` otherwise returns `Object.prototype.toString`,
836
+ * which is truthy, and the parser reads a method it does not support as one it does.
837
+ */ const sourceKeyed = (entries)=>Object.assign(Object.create(null), entries);
838
+ const STRING_ESCAPES = sourceKeyed({
374
839
  "n": "\n",
375
840
  "r": "\r",
376
841
  "t": "\t",
@@ -378,6 +843,24 @@ const STRING_ESCAPES = {
378
843
  "f": "\f",
379
844
  "v": "\v",
380
845
  "0": "\0"
846
+ });
847
+ /**
848
+ * Whether a `/` here opens a regex rather than dividing.
849
+ *
850
+ * A regex cannot follow a value. Everything else — the start of the source, an operator, an opening
851
+ * bracket, a comma — is a position where only a regex makes sense.
852
+ */ const regexCanStartHere = (tokens)=>{
853
+ const previous = tokens[tokens.length - 1];
854
+ if (previous == null) {
855
+ return true;
856
+ }
857
+ if (previous.kind === "number" || previous.kind === "string" || previous.kind === "bigint" || previous.kind === "regex") {
858
+ return false;
859
+ }
860
+ if (previous.kind === "identifier") {
861
+ return false;
862
+ }
863
+ return previous.value !== ")" && previous.value !== "]";
381
864
  };
382
865
  const isIdentifierStart = (char)=>/[a-zA-Z_$]/.test(char);
383
866
  const isIdentifierPart = (char)=>/[a-zA-Z0-9_$]/.test(char);
@@ -427,6 +910,51 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
427
910
  i++;
428
911
  continue;
429
912
  }
913
+ /**
914
+ * A regex literal, told from division by what came before it.
915
+ *
916
+ * `/` after a value — a number, string, identifier, `)` or `]` — is division. Anywhere else
917
+ * it opens a regex. That is the same rule a JavaScript lexer uses, and it is why `x.a / 2`
918
+ * and `/^a/.test(x.a)` can share a character.
919
+ */ if (char === "/" && source[i + 1] !== "/" && source[i + 1] !== "*" && regexCanStartHere(tokens)) {
920
+ let value = "";
921
+ let inClass = false;
922
+ let j = i + 1;
923
+ while(j < source.length){
924
+ const current = source[j];
925
+ if (current === "\\") {
926
+ value += current + (source[j + 1] ?? "");
927
+ j += 2;
928
+ continue;
929
+ }
930
+ if (current === "[") {
931
+ inClass = true;
932
+ } else if (current === "]") {
933
+ inClass = false;
934
+ } else if (current === "/" && inClass === false) {
935
+ break;
936
+ } else if (current === "\n") {
937
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
938
+ }
939
+ value += current;
940
+ j++;
941
+ }
942
+ if (j >= source.length) {
943
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
944
+ }
945
+ j++;
946
+ let flags = "";
947
+ while(j < source.length && isIdentifierPart(source[j])){
948
+ flags += source[j];
949
+ j++;
950
+ }
951
+ i = j;
952
+ tokens.push({
953
+ kind: "regex",
954
+ value: `${value}\u0000${flags}`
955
+ });
956
+ continue;
957
+ }
430
958
  // Comments
431
959
  if (char === "/" && source[i + 1] === "/") {
432
960
  while(i < source.length && source[i] !== "\n"){
@@ -446,6 +974,8 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
446
974
  if (char === "'" || char === "\"" || char === "`") {
447
975
  const quote = char;
448
976
  let value = "";
977
+ const chunks = [];
978
+ const expressions = [];
449
979
  i++;
450
980
  while(i < source.length && source[i] !== quote){
451
981
  if (source[i] === "\\") {
@@ -460,8 +990,43 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
460
990
  i += 2;
461
991
  continue;
462
992
  }
463
- if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
464
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("template literal interpolation"));
993
+ /**
994
+ * An interpolation. The literal so far becomes a chunk and the expression source is
995
+ * kept whole, to be parsed by its own stream — nesting means the inner source can
996
+ * hold anything, including another template.
997
+ */ if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
998
+ let depth = 1;
999
+ let expression = "";
1000
+ let at = i + 2;
1001
+ while(at < source.length && depth > 0){
1002
+ const current = source[at];
1003
+ if (current === "{") {
1004
+ depth++;
1005
+ } else if (current === "}") {
1006
+ depth--;
1007
+ if (depth === 0) {
1008
+ break;
1009
+ }
1010
+ } else if (current === "'" || current === '"' || current === "`") {
1011
+ const closing = current;
1012
+ expression += current;
1013
+ at++;
1014
+ while(at < source.length && source[at] !== closing){
1015
+ expression += source[at] === "\\" ? source[at] + (source[at + 1] ?? "") : source[at];
1016
+ at += source[at] === "\\" ? 2 : 1;
1017
+ }
1018
+ }
1019
+ expression += source[at];
1020
+ at++;
1021
+ }
1022
+ if (depth > 0) {
1023
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated template interpolation"));
1024
+ }
1025
+ chunks.push(value);
1026
+ expressions.push(expression);
1027
+ value = "";
1028
+ i = at + 1;
1029
+ continue;
465
1030
  }
466
1031
  value += source[i];
467
1032
  i++;
@@ -470,6 +1035,17 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
470
1035
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated string literal"));
471
1036
  }
472
1037
  i++; // consume closing quote
1038
+ if (expressions.length > 0) {
1039
+ chunks.push(value);
1040
+ tokens.push({
1041
+ kind: "template",
1042
+ value: JSON.stringify({
1043
+ chunks,
1044
+ expressions
1045
+ })
1046
+ });
1047
+ continue;
1048
+ }
473
1049
  tokens.push({
474
1050
  kind: "string",
475
1051
  value
@@ -513,6 +1089,14 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
513
1089
  }
514
1090
  }
515
1091
  }
1092
+ if (source[i] === "n") {
1093
+ i++;
1094
+ tokens.push({
1095
+ kind: "bigint",
1096
+ value: value.replace(/_/g, "")
1097
+ });
1098
+ continue;
1099
+ }
516
1100
  tokens.push({
517
1101
  kind: "number",
518
1102
  value: value.replace(/_/g, "")
@@ -563,6 +1147,24 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
563
1147
  constructor(tokens){
564
1148
  this.tokens = tokens;
565
1149
  }
1150
+ /** Inserts tokens at the cursor, bracketed so they keep their own precedence. */ splice(tokens) {
1151
+ const bracketed = [
1152
+ {
1153
+ kind: "punctuation",
1154
+ value: "("
1155
+ },
1156
+ ...tokens,
1157
+ {
1158
+ kind: "punctuation",
1159
+ value: ")"
1160
+ }
1161
+ ];
1162
+ this.tokens = [
1163
+ ...this.tokens.slice(0, this.index),
1164
+ ...bracketed,
1165
+ ...this.tokens.slice(this.index)
1166
+ ];
1167
+ }
566
1168
  get isAtEnd() {
567
1169
  return this.index >= this.tokens.length;
568
1170
  }
@@ -577,10 +1179,108 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
577
1179
  this.index++;
578
1180
  return token;
579
1181
  }
1182
+ /** The tokens of one statement's value, through the `;` or block end that closes it. */ takeStatementTokens() {
1183
+ const tokens = [];
1184
+ let depth = 0;
1185
+ while(!this.isAtEnd){
1186
+ const token = this.peek();
1187
+ if (token.kind === "punctuation") {
1188
+ if (token.value === "(" || token.value === "[" || token.value === "{") {
1189
+ depth++;
1190
+ } else if (token.value === ")" || token.value === "]" || token.value === "}") {
1191
+ if (depth === 0) {
1192
+ break;
1193
+ }
1194
+ depth--;
1195
+ } else if (token.value === ";" && depth === 0) {
1196
+ this.next();
1197
+ break;
1198
+ }
1199
+ }
1200
+ tokens.push(this.next());
1201
+ }
1202
+ if (tokens.length === 0) {
1203
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a declaration with no value"));
1204
+ }
1205
+ return tokens;
1206
+ }
580
1207
  isPunctuation(value, offset = 0) {
581
1208
  const token = this.peek(offset);
582
1209
  return token != null && token.kind === "punctuation" && token.value === value;
583
1210
  }
1211
+ /**
1212
+ * Whether the group starting here holds a value rather than a condition.
1213
+ *
1214
+ * `(a && b)` is a boolean sub-expression; `(x.name ?? '') === 'ada'` and `(x.name).length` are
1215
+ * values. Only the token after the matching bracket tells them apart, so the decision is made by
1216
+ * looking ahead rather than by parsing one way and catching the failure — a rewind on exception
1217
+ * would swallow a genuine syntax error inside the group and report it as something else.
1218
+ */ groupIsValue() {
1219
+ let depth = 0;
1220
+ let at = this.index;
1221
+ for(; at < this.tokens.length; at++){
1222
+ const token = this.tokens[at];
1223
+ if (token.kind !== "punctuation") {
1224
+ continue;
1225
+ }
1226
+ if (token.value === "(") {
1227
+ depth++;
1228
+ continue;
1229
+ }
1230
+ if (token.value === ")") {
1231
+ depth--;
1232
+ if (depth === 0) {
1233
+ break;
1234
+ }
1235
+ }
1236
+ }
1237
+ const after = this.tokens[at + 1];
1238
+ if (after == null || after.kind !== "punctuation") {
1239
+ return false;
1240
+ }
1241
+ return COMPARISON_OPERATORS[after.value] != null || after.value === "." || after.value === "?.";
1242
+ }
1243
+ /** Whether a `?` sits at the top level of what is left, so this is a conditional. */ holdsConditional() {
1244
+ let depth = 0;
1245
+ for(let at = this.index; at < this.tokens.length; at++){
1246
+ const token = this.tokens[at];
1247
+ if (token.kind !== "punctuation") {
1248
+ continue;
1249
+ }
1250
+ if (token.value === "(" || token.value === "[") {
1251
+ depth++;
1252
+ } else if (token.value === ")" || token.value === "]") {
1253
+ depth--;
1254
+ } else if (token.value === "?" && depth === 0) {
1255
+ return true;
1256
+ }
1257
+ }
1258
+ return false;
1259
+ }
1260
+ /** Whether the group starting here is `( … ? … : … )` rather than a plain value. */ groupHoldsConditional() {
1261
+ let depth = 0;
1262
+ for(let at = this.index; at < this.tokens.length; at++){
1263
+ const token = this.tokens[at];
1264
+ if (token.kind !== "punctuation") {
1265
+ continue;
1266
+ }
1267
+ if (token.value === "(") {
1268
+ depth++;
1269
+ continue;
1270
+ }
1271
+ if (token.value === ")") {
1272
+ depth--;
1273
+ if (depth === 0) {
1274
+ return false;
1275
+ }
1276
+ continue;
1277
+ }
1278
+ if (token.value === "?" && depth === 1) {
1279
+ return true;
1280
+ }
1281
+ }
1282
+ return false;
1283
+ }
584
1284
  matchPunctuation(value) {
585
1285
  if (this.isPunctuation(value)) {
586
1286
  this.index++;
@@ -594,12 +1294,101 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
594
1294
  }
595
1295
  }
596
1296
  }
597
- const COMPARATOR_METHODS = {
1297
+ /**
1298
+ * Calls JavaScript binds LOOSER than a comparison.
1299
+ *
1300
+ * This grammar reads a comparison's operands as values, which puts these tighter than they belong:
1301
+ * `x.flags & 6 === 2` is `x.flags & (6 === 2)` in JavaScript and would be read here as
1302
+ * `(x.flags & 6) === 2`. The two answer differently, so an ungrouped one is refused rather than
1303
+ * reinterpreted — the filter then runs in memory against the caller's own function, which is right by
1304
+ * construction. Brackets say which was meant, and JavaScript itself makes an unbracketed `??` mix a
1305
+ * syntax error for the same reason.
1306
+ */ const LOOSER_THAN_COMPARISON = [
1307
+ "bit-and",
1308
+ "bit-or",
1309
+ "bit-xor",
1310
+ "coalesce"
1311
+ ];
1312
+ const needsBrackets = (operand)=>operand.kind === "arithmetic" && operand.grouped !== true && LOOSER_THAN_COMPARISON.includes(operand.call);
1313
+ /** JavaScript precedence: `*`, `/`, `%` bind tighter than `+` and `-`. */ const MULTIPLICATIVE_OPERATORS = sourceKeyed({
1314
+ "*": "multiply",
1315
+ "/": "divide",
1316
+ "%": "modulo"
1317
+ });
1318
+ const ADDITIVE_OPERATORS = sourceKeyed({
1319
+ "+": "add",
1320
+ "-": "subtract"
1321
+ });
1322
+ const SHIFT_OPERATORS = sourceKeyed({
1323
+ "<<": "shift-left",
1324
+ ">>": "shift-right",
1325
+ ">>>": "shift-right-unsigned"
1326
+ });
1327
+ const BITWISE_AND_OPERATORS = sourceKeyed({
1328
+ "&": "bit-and"
1329
+ });
1330
+ const BITWISE_XOR_OPERATORS = sourceKeyed({
1331
+ "^": "bit-xor"
1332
+ });
1333
+ const BITWISE_OR_OPERATORS = sourceKeyed({
1334
+ "|": "bit-or"
1335
+ });
1336
+ const COALESCE_OPERATORS = sourceKeyed({
1337
+ "??": "coalesce"
1338
+ });
1339
+ /** Whether a schema property is reachable in here, which decides which side of a comparison it is. */ const containsProperty = (operand)=>{
1340
+ if (operand.kind === "property") {
1341
+ return true;
1342
+ }
1343
+ if (operand.kind === "conditional") {
1344
+ // A comparison always names a schema property, so the condition alone settles it
1345
+ return true;
1346
+ }
1347
+ return operand.kind === "arithmetic" && (containsProperty(operand.left) || containsProperty(operand.right) || operand.extra != null && containsProperty(operand.extra));
1348
+ };
1349
+ const DECLARATION_KEYWORDS = new Set([
1350
+ "const",
1351
+ "let",
1352
+ "var"
1353
+ ]);
1354
+ /** An operand whose value only a row can supply. */ const UNKNOWN_UNTIL_ROW = Symbol("unknown until row");
1355
+ /** The empty argument slot of a unary call. Compared by identity, so a real `undefined` still counts. */ const NO_ARGUMENT = Object.freeze({
1356
+ kind: "value",
1357
+ value: undefined,
1358
+ transformer: null,
1359
+ locale: null
1360
+ });
1361
+ const noArgument = ()=>NO_ARGUMENT;
1362
+ /** A predicate no row satisfies. Never reaches a tree: no expression node means "match nothing". */ const NEVER = "never";
1363
+ const and = (left, right)=>{
1364
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left)) {
1365
+ return right;
1366
+ }
1367
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1368
+ return left;
1369
+ }
1370
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1371
+ operator: "&&",
1372
+ left,
1373
+ right
1374
+ });
1375
+ };
1376
+ const or = (left, right)=>{
1377
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left) || _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1378
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
1379
+ }
1380
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1381
+ operator: "||",
1382
+ left,
1383
+ right
1384
+ });
1385
+ };
1386
+ const COMPARATOR_METHODS = sourceKeyed({
598
1387
  startsWith: "starts-with",
599
1388
  endsWith: "ends-with",
600
1389
  includes: "includes"
601
- };
602
- const TRANSFORM_METHODS = {
1390
+ });
1391
+ const TRANSFORM_METHODS = sourceKeyed({
603
1392
  toLowerCase: {
604
1393
  transformer: "to-lower-case",
605
1394
  locale: null
@@ -616,8 +1405,8 @@ const TRANSFORM_METHODS = {
616
1405
  transformer: "to-upper-case",
617
1406
  locale: "en-US"
618
1407
  }
619
- };
620
- const COMPARISON_OPERATORS = {
1408
+ });
1409
+ const COMPARISON_OPERATORS = sourceKeyed({
621
1410
  "==": {
622
1411
  comparator: "equals",
623
1412
  negated: false,
@@ -658,7 +1447,7 @@ const COMPARISON_OPERATORS = {
658
1447
  negated: false,
659
1448
  strict: false
660
1449
  }
661
- };
1450
+ });
662
1451
  const SWAPPED_COMPARATORS = {
663
1452
  "equals": "equals",
664
1453
  "greater-than": "less-than",
@@ -729,14 +1518,14 @@ const resolveParamPath = (paramsName, path, data)=>{
729
1518
  */ class ExpressionParser {
730
1519
  schema;
731
1520
  stream;
732
- entityName;
1521
+ scope;
733
1522
  paramsName;
734
1523
  params;
735
1524
  /** Set when a param value shaped the tree itself (e.g. x[p.name]) — such templates cannot be cached. */ structurallyDependsOnParams = false;
736
- constructor(schema, stream, entityName, paramsName, params){
1525
+ constructor(schema, stream, scope, paramsName, params){
737
1526
  this.schema = schema;
738
1527
  this.stream = stream;
739
- this.entityName = entityName;
1528
+ this.scope = scope;
740
1529
  this.paramsName = paramsName;
741
1530
  this.params = params;
742
1531
  }
@@ -747,6 +1536,180 @@ const resolveParamPath = (paramsName, path, data)=>{
747
1536
  }
748
1537
  return expression;
749
1538
  }
1539
+ parseBody() {
1540
+ if (!this.stream.isPunctuation("{")) {
1541
+ return this.parse();
1542
+ }
1543
+ const answer = this.parseBlock();
1544
+ if (!this.stream.isAtEnd) {
1545
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`unexpected token '${this.stream.peek()?.value}'`));
1546
+ }
1547
+ if (answer === NEVER) {
1548
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a predicate no row can satisfy"));
1549
+ }
1550
+ return answer;
1551
+ }
1552
+ /** The expression a `{ … }` block answers with. */ parseBlock() {
1553
+ this.stream.expectPunctuation("{");
1554
+ const answer = this.parseStatements();
1555
+ this.stream.expectPunctuation("}");
1556
+ return answer;
1557
+ }
1558
+ /** Statements up to the one that returns. What follows a `return` is never read, as in JavaScript. */ parseStatements() {
1559
+ if (this.stream.isPunctuation("}") || this.stream.isAtEnd) {
1560
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a block body that returns nothing"));
1561
+ }
1562
+ const keyword = this.stream.peek();
1563
+ if (keyword == null || keyword.kind !== "identifier") {
1564
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`a statement starting '${keyword?.value}'`));
1565
+ }
1566
+ if (DECLARATION_KEYWORDS.has(keyword.value)) {
1567
+ this.declare();
1568
+ return this.parseStatements();
1569
+ }
1570
+ if (keyword.value === "return") {
1571
+ this.stream.next();
1572
+ const answer = this.parseReturnedCondition();
1573
+ this.stream.matchPunctuation(";");
1574
+ return answer;
1575
+ }
1576
+ if (keyword.value === "if") {
1577
+ return this.parseIfStatement();
1578
+ }
1579
+ if (keyword.value === "switch") {
1580
+ return this.parseSwitchStatement();
1581
+ }
1582
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the statement '${keyword.value}'`));
1583
+ }
1584
+ /**
1585
+ * Binds a `const`/`let`/`var` name to the tokens of its initializer — tokens rather than a parsed
1586
+ * expression, so the name works as an operand, an argument, or a call receiver alike.
1587
+ */ declare() {
1588
+ this.stream.next();
1589
+ const name = this.stream.next();
1590
+ if (name.kind !== "identifier") {
1591
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the declaration '${name.value}'`));
1592
+ }
1593
+ this.stream.expectPunctuation("=");
1594
+ this.scope.set(name.value, {
1595
+ kind: "inlined",
1596
+ tokens: this.stream.takeStatementTokens()
1597
+ });
1598
+ }
1599
+ /** `return false` on its own, which no row satisfies, and every other returned condition. */ parseReturnedCondition() {
1600
+ const next = this.stream.peek();
1601
+ const after = this.stream.peek(1);
1602
+ const endsHere = after == null || after.kind === "punctuation" && (after.value === ";" || after.value === "}");
1603
+ if (next != null && next.kind === "identifier" && next.value === "false" && endsHere) {
1604
+ this.stream.next();
1605
+ return NEVER;
1606
+ }
1607
+ return this.parseOr();
1608
+ }
1609
+ parseIfStatement() {
1610
+ this.stream.next();
1611
+ this.stream.expectPunctuation("(");
1612
+ const condition = this.parseOr();
1613
+ this.stream.expectPunctuation(")");
1614
+ const whenTrue = this.parseBranch();
1615
+ if (this.stream.peek()?.value === "else") {
1616
+ this.stream.next();
1617
+ return this.either(condition, whenTrue, this.parseBranch());
1618
+ }
1619
+ // Without an `else`, the statements after the `if` are the other branch
1620
+ return this.either(condition, whenTrue, this.parseStatements());
1621
+ }
1622
+ /** One arm of an `if`: a block, or a single statement. */ parseBranch() {
1623
+ return this.stream.isPunctuation("{") ? this.parseBlock() : this.parseStatements();
1624
+ }
1625
+ /** A `switch` over one subject, as the disjunction of its cases. */ parseSwitchStatement() {
1626
+ this.stream.next();
1627
+ this.stream.expectPunctuation("(");
1628
+ const subject = this.parseValue();
1629
+ this.stream.expectPunctuation(")");
1630
+ this.stream.expectPunctuation("{");
1631
+ let matching = null;
1632
+ let pending = [];
1633
+ let everyLabel = [];
1634
+ let byDefault = null;
1635
+ let anyCaseBroke = false;
1636
+ while(!this.stream.matchPunctuation("}")){
1637
+ const label = this.stream.next();
1638
+ if (label.kind !== "identifier" || label.value !== "case" && label.value !== "default") {
1639
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'${label.value}' inside a switch`));
1640
+ }
1641
+ if (label.value === "case") {
1642
+ const test = this.buildComparison(subject, COMPARISON_OPERATORS["==="], this.parseValue());
1643
+ pending.push(test);
1644
+ everyLabel.push(test);
1645
+ }
1646
+ this.stream.expectPunctuation(":");
1647
+ // `case 'a':` with no body of its own runs the next case's body
1648
+ if (this.stream.peek()?.value === "case" || this.stream.peek()?.value === "default") {
1649
+ continue;
1650
+ }
1651
+ if (this.stream.peek()?.value === "break") {
1652
+ this.stream.next();
1653
+ this.stream.matchPunctuation(";");
1654
+ anyCaseBroke = true;
1655
+ pending = [];
1656
+ continue;
1657
+ }
1658
+ const body = this.parseCaseBody();
1659
+ if (label.value === "default") {
1660
+ byDefault = body === NEVER ? null : body;
1661
+ continue;
1662
+ }
1663
+ if (body !== NEVER && pending.length > 0) {
1664
+ const reached = pending.reduce((left, right)=>or(left, right));
1665
+ const term = _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(body) ? reached : and(reached, body);
1666
+ matching = matching == null ? term : or(matching, term);
1667
+ }
1668
+ pending = [];
1669
+ }
1670
+ // Falling out of the switch continues after it, so the statements there are the default too
1671
+ const afterSwitch = byDefault == null && !this.stream.isPunctuation("}") && !this.stream.isAtEnd ? this.parseStatements() : NEVER;
1672
+ if (afterSwitch !== NEVER) {
1673
+ // A `break` also continues after the switch, so its case would take that answer rather
1674
+ // than none — a distinction this rewrite cannot carry
1675
+ if (anyCaseBroke) {
1676
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a switch that breaks and then falls into more statements"));
1677
+ }
1678
+ byDefault = afterSwitch;
1679
+ }
1680
+ // A `default` runs only when every case failed, wherever it was written
1681
+ if (byDefault != null) {
1682
+ const noCaseMatched = everyLabel.length === 0 ? byDefault : and(this.negateExpression(everyLabel.reduce((left, right)=>or(left, right))), byDefault);
1683
+ matching = matching == null ? noCaseMatched : or(matching, noCaseMatched);
1684
+ }
1685
+ return matching ?? NEVER;
1686
+ }
1687
+ /** One case body, and the `break` that may follow its `return`. */ parseCaseBody() {
1688
+ const answer = this.parseStatements();
1689
+ if (this.stream.peek()?.value === "break") {
1690
+ this.stream.next();
1691
+ this.stream.matchPunctuation(";");
1692
+ }
1693
+ return answer;
1694
+ }
1695
+ /**
1696
+ * The predicate an `if`/`else` answers: `(condition && whenTrue) || (!condition && whenFalse)`,
1697
+ * with each case below that form after a constant branch cancels out.
1698
+ */ either(condition, whenTrue, whenFalse) {
1699
+ if (whenTrue === NEVER) {
1700
+ return whenFalse === NEVER ? NEVER : and(this.negateExpression(condition), whenFalse);
1701
+ }
1702
+ if (whenFalse === NEVER) {
1703
+ return and(condition, whenTrue);
1704
+ }
1705
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenTrue)) {
1706
+ return or(condition, whenFalse);
1707
+ }
1708
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenFalse)) {
1709
+ return or(this.negateExpression(condition), whenTrue);
1710
+ }
1711
+ return or(and(condition, whenTrue), and(this.negateExpression(condition), whenFalse));
1712
+ }
750
1713
  // || binds loosest, so it sits at the root of the parse
751
1714
  parseOr() {
752
1715
  let left = this.parseAnd();
@@ -794,10 +1757,18 @@ const resolveParamPath = (paramsName, path, data)=>{
794
1757
  /**
795
1758
  * Applies `!` to an already-parsed expression: comparators flip their
796
1759
  * negated flag, compound expressions distribute via De Morgan's laws.
1760
+ *
1761
+ * Builds a new tree rather than flipping the flag in place, because an `if` uses its condition
1762
+ * twice — once negated — and a shared node would carry the flip into both branches.
797
1763
  */ negateExpression(expression) {
798
1764
  if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
799
- expression.negated = !expression.negated;
800
- return expression;
1765
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1766
+ comparator: expression.comparator,
1767
+ negated: !expression.negated,
1768
+ strict: expression.strict,
1769
+ left: expression.left,
1770
+ right: expression.right
1771
+ });
801
1772
  }
802
1773
  if (expression instanceof _types__rspack_import_1/* .OperatorExpression */.fw && expression.left != null && expression.right != null) {
803
1774
  return new _types__rspack_import_1/* .OperatorExpression */.fw({
@@ -809,25 +1780,125 @@ const resolveParamPath = (paramsName, path, data)=>{
809
1780
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("'!' on this expression"));
810
1781
  }
811
1782
  parseComparison() {
812
- // Parenthesized group
813
- if (this.stream.matchPunctuation("(")) {
1783
+ /**
1784
+ * A parenthesised group is either a boolean sub-expression or a VALUE — `(a && b)` against
1785
+ * `(x.name ?? '') === 'ada'` — and which one it is is only known at the closing bracket, by
1786
+ * what follows. So the boolean reading is tried first and rewound if a comparator turns up.
1787
+ */ if (this.stream.isPunctuation("(") && this.stream.groupIsValue() === false) {
1788
+ this.stream.next();
814
1789
  const expression = this.parseOr();
815
1790
  this.stream.expectPunctuation(")");
816
- const trailing = this.stream.peek();
817
- if (trailing != null && trailing.kind === "punctuation" && COMPARISON_OPERATORS[trailing.value] != null) {
818
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison against a parenthesized expression"));
819
- }
820
1791
  return expression;
821
1792
  }
822
- const left = this.parseOperand();
1793
+ const left = this.parseValue();
823
1794
  const operatorToken = this.stream.peek();
824
1795
  if (operatorToken != null && operatorToken.kind === "punctuation" && COMPARISON_OPERATORS[operatorToken.value] != null) {
825
1796
  this.stream.next();
826
- const right = this.parseOperand();
1797
+ const right = this.parseValue();
827
1798
  return this.buildComparison(left, COMPARISON_OPERATORS[operatorToken.value], right);
828
1799
  }
829
1800
  return this.buildStandalone(left);
830
1801
  }
1802
+ /**
1803
+ * A value, at JavaScript's precedence.
1804
+ *
1805
+ * Lowest first: the conditional operator, then nullish coalescing, then the bitwise levels, then
1806
+ * the shifts, then the arithmetic. Comparison sits between the shifts and the bitwise levels in
1807
+ * JavaScript, but a comparison is a boolean and is handled by `parseComparison` above, so this
1808
+ * chain skips it — a bitwise operand here is always a value.
1809
+ */ /**
1810
+ * An operand from its own source, sharing this parser's schema and parameter names.
1811
+ *
1812
+ * A structural dependence found inside propagates outward: the template it belongs to cannot be
1813
+ * cached either.
1814
+ */ parseNested(source) {
1815
+ const nested = new ExpressionParser(this.schema, new TokenStream(tokenize(source)), this.scope, this.paramsName, this.params);
1816
+ const operand = nested.parseInterpolation();
1817
+ // Leftover tokens mean the interpolation held something this reads only part of. Silently
1818
+ // keeping the part it understood is the worst outcome available: `${x.age > 5 ? "a" : "b"}`
1819
+ // would become `x.age`, and the filter would answer a question nobody asked.
1820
+ if (nested.stream.isAtEnd === false) {
1821
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("an interpolation this parser reads only part of"));
1822
+ }
1823
+ if (nested.structurallyDependsOnParams === true) {
1824
+ this.structurallyDependsOnParams = true;
1825
+ }
1826
+ return operand;
1827
+ }
1828
+ /**
1829
+ * The whole of one `${…}`.
1830
+ *
1831
+ * A conditional is read here rather than in `parseValue`, because an interpolation is the one
1832
+ * place a conditional appears without brackets around it.
1833
+ */ parseInterpolation() {
1834
+ if (this.stream.holdsConditional()) {
1835
+ const condition = this.parseOr();
1836
+ this.stream.expectPunctuation("?");
1837
+ const whenTrue = this.parseValue();
1838
+ this.stream.expectPunctuation(":");
1839
+ const whenFalse = this.parseValue();
1840
+ return {
1841
+ kind: "conditional",
1842
+ condition,
1843
+ whenTrue,
1844
+ whenFalse
1845
+ };
1846
+ }
1847
+ return this.parseValue();
1848
+ }
1849
+ parseValue() {
1850
+ return this.parseCoalesce();
1851
+ }
1852
+ parseCoalesce() {
1853
+ return this.parseBinary(COALESCE_OPERATORS, ()=>this.parseBitwiseOr());
1854
+ }
1855
+ parseBitwiseOr() {
1856
+ return this.parseBinary(BITWISE_OR_OPERATORS, ()=>this.parseBitwiseXor());
1857
+ }
1858
+ parseBitwiseXor() {
1859
+ return this.parseBinary(BITWISE_XOR_OPERATORS, ()=>this.parseBitwiseAnd());
1860
+ }
1861
+ parseBitwiseAnd() {
1862
+ return this.parseBinary(BITWISE_AND_OPERATORS, ()=>this.parseShift());
1863
+ }
1864
+ parseShift() {
1865
+ return this.parseBinary(SHIFT_OPERATORS, ()=>this.parseAdditive());
1866
+ }
1867
+ parseAdditive() {
1868
+ return this.parseBinary(ADDITIVE_OPERATORS, ()=>this.parseMultiplicative());
1869
+ }
1870
+ parseMultiplicative() {
1871
+ return this.parseBinary(MULTIPLICATIVE_OPERATORS, ()=>this.parseExponent());
1872
+ }
1873
+ /** `**` is RIGHT-associative: `2 ** 3 ** 2` is 2 ** 9, not 8 ** 2. */ parseExponent() {
1874
+ const left = this.parseOperand();
1875
+ if (this.stream.isPunctuation("**") === false) {
1876
+ return left;
1877
+ }
1878
+ this.stream.next();
1879
+ return {
1880
+ kind: "arithmetic",
1881
+ call: "power",
1882
+ left,
1883
+ right: this.parseExponent()
1884
+ };
1885
+ }
1886
+ /** Left-associative, so `a - b - c` is `(a - b) - c` rather than `a - (b - c)`. */ parseBinary(operators, next) {
1887
+ let left = next();
1888
+ for(;;){
1889
+ const token = this.stream.peek();
1890
+ if (token == null || token.kind !== "punctuation" || operators[token.value] == null) {
1891
+ return left;
1892
+ }
1893
+ this.stream.next();
1894
+ left = {
1895
+ kind: "arithmetic",
1896
+ call: operators[token.value],
1897
+ left,
1898
+ right: next()
1899
+ };
1900
+ }
1901
+ }
831
1902
  parseOperand() {
832
1903
  const token = this.stream.peek();
833
1904
  if (token == null) {
@@ -851,6 +1922,131 @@ const resolveParamPath = (paramsName, path, data)=>{
851
1922
  locale: null
852
1923
  };
853
1924
  }
1925
+ // A parenthesised VALUE — `(x.price & 1)`, `(x.name ?? '')`. The boolean reading of a group
1926
+ // is handled in parseComparison; by the time an operand sees one it is arithmetic.
1927
+ if (token.kind === "punctuation" && token.value === "(") {
1928
+ const conditional = this.stream.groupHoldsConditional();
1929
+ this.stream.next();
1930
+ if (conditional === true) {
1931
+ const condition = this.parseOr();
1932
+ this.stream.expectPunctuation("?");
1933
+ const whenTrue = this.parseValue();
1934
+ this.stream.expectPunctuation(":");
1935
+ const whenFalse = this.parseValue();
1936
+ this.stream.expectPunctuation(")");
1937
+ return {
1938
+ kind: "conditional",
1939
+ condition,
1940
+ whenTrue,
1941
+ whenFalse
1942
+ };
1943
+ }
1944
+ const inner = this.parseValue();
1945
+ this.stream.expectPunctuation(")");
1946
+ const grouped = inner.kind === "arithmetic" ? {
1947
+ ...inner,
1948
+ grouped: true
1949
+ } : inner;
1950
+ return this.withGroupCall(grouped);
1951
+ }
1952
+ /**
1953
+ * A template with interpolation, folded into `concat`.
1954
+ *
1955
+ * Each `${…}` was kept as source by the tokenizer and is parsed by its own stream, so it can
1956
+ * hold anything an operand can — a property, a param, arithmetic, another template. Empty
1957
+ * chunks are dropped: `${a}${b}` is two operands, not two operands and three empty strings.
1958
+ */ if (token.kind === "template") {
1959
+ this.stream.next();
1960
+ const { chunks, expressions } = JSON.parse(token.value);
1961
+ const pieces = [];
1962
+ for(let at = 0; at < chunks.length; at++){
1963
+ if (chunks[at].length > 0) {
1964
+ pieces.push({
1965
+ kind: "value",
1966
+ value: chunks[at],
1967
+ transformer: null,
1968
+ locale: null
1969
+ });
1970
+ }
1971
+ if (at < expressions.length) {
1972
+ pieces.push(this.parseNested(expressions[at]));
1973
+ }
1974
+ }
1975
+ if (pieces.length === 0) {
1976
+ return {
1977
+ kind: "value",
1978
+ value: "",
1979
+ transformer: null,
1980
+ locale: null
1981
+ };
1982
+ }
1983
+ // One piece and no chunk means no concat to do the coercion, so the conversion has to be
1984
+ // explicit: `` `${x.age}` `` is the STRING "9", not the number 9.
1985
+ if (pieces.length === 1) {
1986
+ const only = pieces[0];
1987
+ const alreadyText = only.kind === "value" && typeof only.value === "string";
1988
+ return alreadyText ? only : {
1989
+ kind: "arithmetic",
1990
+ call: "to-string",
1991
+ left: only,
1992
+ right: noArgument()
1993
+ };
1994
+ }
1995
+ return pieces.reduce((left, right)=>({
1996
+ kind: "arithmetic",
1997
+ call: "concat",
1998
+ left,
1999
+ right
2000
+ }));
2001
+ }
2002
+ if (token.kind === "bigint") {
2003
+ this.stream.next();
2004
+ return {
2005
+ kind: "value",
2006
+ value: BigInt(token.value),
2007
+ transformer: null,
2008
+ locale: null
2009
+ };
2010
+ }
2011
+ if (token.kind === "regex") {
2012
+ this.stream.next();
2013
+ const [source, flags] = token.value.split("\u0000");
2014
+ const pattern = {
2015
+ kind: "value",
2016
+ value: new RegExp(source, flags),
2017
+ transformer: null,
2018
+ locale: null
2019
+ };
2020
+ // `/^a/.test(x.name)` — the pattern is the literal, the subject is the argument, and the
2021
+ // tree puts them the other way round: the property is what the call applies to.
2022
+ if (this.stream.isPunctuation(".")) {
2023
+ const method = this.stream.peek(1);
2024
+ if (method != null && method.kind === "identifier" && method.value === "test") {
2025
+ this.stream.next();
2026
+ this.stream.next();
2027
+ this.stream.expectPunctuation("(");
2028
+ const subject = this.parseValue();
2029
+ this.stream.expectPunctuation(")");
2030
+ return {
2031
+ kind: "arithmetic",
2032
+ call: "matches",
2033
+ left: subject,
2034
+ right: pattern
2035
+ };
2036
+ }
2037
+ }
2038
+ return pattern;
2039
+ }
2040
+ if (token.kind === "punctuation" && token.value === "~") {
2041
+ this.stream.next();
2042
+ // Unary, so the tree carries the operand and no argument
2043
+ return {
2044
+ kind: "arithmetic",
2045
+ call: "bit-not",
2046
+ left: this.parseOperand(),
2047
+ right: noArgument()
2048
+ };
2049
+ }
854
2050
  if (token.kind === "punctuation" && token.value === "-") {
855
2051
  this.stream.next();
856
2052
  const numberToken = this.stream.next();
@@ -908,6 +2104,9 @@ const resolveParamPath = (paramsName, path, data)=>{
908
2104
  if (argument.kind === "method-call") {
909
2105
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("nested method call inside .includes()"));
910
2106
  }
2107
+ if (argument.kind === "arithmetic" || argument.kind === "conditional") {
2108
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic inside .includes()"));
2109
+ }
911
2110
  return {
912
2111
  kind: "method-call",
913
2112
  target: array,
@@ -953,16 +2152,17 @@ const resolveParamPath = (paramsName, path, data)=>{
953
2152
  locale: null
954
2153
  };
955
2154
  }
956
- if (root === this.entityName) {
957
- return this.parseChain({
958
- kind: "property",
959
- root
960
- });
961
- }
962
- if (this.paramsName != null && root === this.paramsName) {
2155
+ const binding = this.scope.get(root);
2156
+ if (binding != null) {
2157
+ if (binding.kind === "inlined") {
2158
+ this.stream.splice(binding.tokens);
2159
+ return this.parseOperand();
2160
+ }
963
2161
  return this.parseChain({
964
- kind: "param",
965
- root
2162
+ kind: binding.kind,
2163
+ path: [
2164
+ ...binding.path
2165
+ ]
966
2166
  });
967
2167
  }
968
2168
  // A bare variable from the outer scope — its value cannot be derived from source text
@@ -972,7 +2172,7 @@ const resolveParamPath = (paramsName, path, data)=>{
972
2172
  * Parses the segments after an entity/params root: dot access, bracket
973
2173
  * access, transform methods and comparator methods.
974
2174
  */ parseChain(options) {
975
- const path = [];
2175
+ const path = options.path;
976
2176
  let transformer = null;
977
2177
  let locale = null;
978
2178
  while(true){
@@ -998,6 +2198,9 @@ const resolveParamPath = (paramsName, path, data)=>{
998
2198
  if (argument.kind === "method-call") {
999
2199
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`nested method call inside .${method}()`));
1000
2200
  }
2201
+ if (argument.kind === "arithmetic" || argument.kind === "conditional") {
2202
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`arithmetic inside .${method}()`));
2203
+ }
1001
2204
  return {
1002
2205
  kind: "method-call",
1003
2206
  target: this.resolveChain(options.kind, path, transformer, locale),
@@ -1034,12 +2237,15 @@ const resolveParamPath = (paramsName, path, data)=>{
1034
2237
  // docs/mutation-backlog.md) — every mutation of this four-conjunct guard reroutes
1035
2238
  // bracket access between two paths that both collapse to NOT_PARSABLE; the
1036
2239
  // experiment recorded there aimed 30 tests at this line and killed none.
1037
- if (kind === "property" && token.kind === "identifier" && this.paramsName != null && token.value === this.paramsName) {
1038
- const paramPath = [];
2240
+ const binding = token.kind === "identifier" ? this.scope.get(token.value) : undefined;
2241
+ if (kind === "property" && binding != null && binding.kind === "param") {
2242
+ const paramPath = [
2243
+ ...binding.path
2244
+ ];
1039
2245
  while(this.stream.matchPunctuation(".") || this.stream.matchPunctuation("?.")){
1040
2246
  paramPath.push(this.stream.next().value);
1041
2247
  }
1042
- const resolved = resolveParamPath(this.paramsName, paramPath, this.params);
2248
+ const resolved = resolveParamPath(this.paramsName ?? token.value, paramPath, this.params);
1043
2249
  if (typeof resolved !== "string") {
1044
2250
  throw new ParamDependentParseError(ERROR_MESSAGES.PROPERTY_NOT_FOUND(paramPath.join(".")));
1045
2251
  }
@@ -1092,6 +2298,67 @@ const resolveParamPath = (paramsName, path, data)=>{
1092
2298
  locale
1093
2299
  };
1094
2300
  }
2301
+ /**
2302
+ * A call on a parenthesised value: `(x.name).toLowerCase()`, `(x.age + 1).length`. Any operand can
2303
+ * receive one here, unlike a property chain, which carries at most one transform.
2304
+ */ withGroupCall(operand) {
2305
+ let receiver = operand;
2306
+ while(this.stream.isPunctuation(".") || this.stream.isPunctuation("?.")){
2307
+ const segment = this.stream.peek(1);
2308
+ if (segment == null || segment.kind !== "identifier") {
2309
+ break;
2310
+ }
2311
+ if (segment.value === "length" && !this.stream.isPunctuation("(", 2)) {
2312
+ this.stream.next();
2313
+ this.stream.next();
2314
+ receiver = {
2315
+ kind: "arithmetic",
2316
+ call: "length",
2317
+ left: receiver,
2318
+ right: noArgument()
2319
+ };
2320
+ continue;
2321
+ }
2322
+ const transform = TRANSFORM_METHODS[segment.value];
2323
+ if (transform != null) {
2324
+ this.stream.next();
2325
+ this.stream.next();
2326
+ this.stream.expectPunctuation("(");
2327
+ this.stream.expectPunctuation(")");
2328
+ receiver = {
2329
+ kind: "arithmetic",
2330
+ call: transform.transformer,
2331
+ left: receiver,
2332
+ right: transform.locale == null ? noArgument() : {
2333
+ kind: "value",
2334
+ value: transform.locale,
2335
+ transformer: null,
2336
+ locale: null
2337
+ }
2338
+ };
2339
+ continue;
2340
+ }
2341
+ // A comparator method needs a property target, which only an ungrouped chain produces
2342
+ if (COMPARATOR_METHODS[segment.value] != null && receiver.kind === "property") {
2343
+ this.stream.next();
2344
+ this.stream.next();
2345
+ this.stream.expectPunctuation("(");
2346
+ const argument = this.parseOperand();
2347
+ this.stream.expectPunctuation(")");
2348
+ if (argument.kind !== "property" && argument.kind !== "value" && argument.kind !== "param") {
2349
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'.${segment.value}()' on that argument`));
2350
+ }
2351
+ return {
2352
+ kind: "method-call",
2353
+ target: receiver,
2354
+ method: segment.value,
2355
+ argument
2356
+ };
2357
+ }
2358
+ break;
2359
+ }
2360
+ return receiver;
2361
+ }
1095
2362
  withValueTransformer(operand) {
1096
2363
  if (this.stream.isPunctuation(".")) {
1097
2364
  const method = this.stream.peek(1);
@@ -1125,12 +2392,22 @@ const resolveParamPath = (paramsName, path, data)=>{
1125
2392
  if (right.kind === "method-call") {
1126
2393
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("method call on the right side of a comparison"));
1127
2394
  }
1128
- if (left.kind === "property" && right.kind === "property") {
1129
- // Casing transformers are only valid with string-matching comparators,
1130
- // which cannot produce a property-to-property comparison
1131
- if (left.transformer === "to-lower-case" || left.transformer === "to-upper-case" || right.transformer === "to-lower-case" || right.transformer === "to-upper-case") {
1132
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
2395
+ if (needsBrackets(left) || needsBrackets(right)) {
2396
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a bitwise or nullish operator compared without brackets, which JavaScript reads the other way round"));
2397
+ }
2398
+ if (left.kind === "arithmetic" || right.kind === "arithmetic" || left.kind === "conditional" || right.kind === "conditional") {
2399
+ if (containsProperty(left) === false && containsProperty(right) === false) {
2400
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic that references no schema property"));
1133
2401
  }
2402
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2403
+ comparator: operator.comparator,
2404
+ negated: operator.negated,
2405
+ strict: operator.strict,
2406
+ left: this.createOperandExpression(left),
2407
+ right: this.createOperandExpression(right)
2408
+ });
2409
+ }
2410
+ if (left.kind === "property" && right.kind === "property") {
1134
2411
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1135
2412
  comparator: operator.comparator,
1136
2413
  negated: operator.negated,
@@ -1139,18 +2416,63 @@ const resolveParamPath = (paramsName, path, data)=>{
1139
2416
  right: this.createPropertyExpression(right)
1140
2417
  });
1141
2418
  }
2419
+ // Only a loose comparison coerces. `===` records `strict` and honouring it is the point.
1142
2420
  if (left.kind === "property" && right.kind !== "property") {
1143
- return this.buildPropertyComparator(left, operator, right, /* applyConverter */ true);
2421
+ return this.buildPropertyComparator(left, operator, right, /* applyConverter */ !operator.strict);
1144
2422
  }
1145
2423
  if (right.kind === "property" && left.kind !== "property") {
1146
2424
  const swapped = {
1147
2425
  ...operator,
1148
2426
  comparator: SWAPPED_COMPARATORS[operator.comparator]
1149
2427
  };
1150
- return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ true);
2428
+ return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ !operator.strict);
2429
+ }
2430
+ const settled = this.settleConstantComparison(left, operator, right);
2431
+ if (settled != null) {
2432
+ return settled;
1151
2433
  }
1152
2434
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison requires a schema property on at least one side"));
1153
2435
  }
2436
+ /**
2437
+ * The answer a comparison of two constants gives, when that answer is `true`. The other answer
2438
+ * excludes every row, which has no expression node.
2439
+ */ settleConstantComparison(left, operator, right) {
2440
+ const leftValue = this.constantOf(left);
2441
+ const rightValue = this.constantOf(right);
2442
+ if (leftValue === UNKNOWN_UNTIL_ROW || rightValue === UNKNOWN_UNTIL_ROW) {
2443
+ return null;
2444
+ }
2445
+ const answer = (0,_evaluate__rspack_import_3/* .evaluate */._3)(new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2446
+ comparator: operator.comparator,
2447
+ negated: operator.negated,
2448
+ strict: operator.strict,
2449
+ left: new _types__rspack_import_1/* .ValueExpression */.Ko({
2450
+ value: leftValue
2451
+ }),
2452
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2453
+ value: rightValue
2454
+ })
2455
+ }), {});
2456
+ if (answer === true) {
2457
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
2458
+ }
2459
+ // Params decided this, so the refusal must not be cached against the source: the same filter
2460
+ // with other params can be a tautology.
2461
+ if (left.kind === "param" || right.kind === "param") {
2462
+ throw new ParamDependentParseError(ERROR_MESSAGES.UNSUPPORTED("a params comparison no row satisfies"));
2463
+ }
2464
+ return null;
2465
+ }
2466
+ /** The value an operand holds already, for the operands that do not depend on a row. */ constantOf(operand) {
2467
+ if (operand.kind === "value" && operand.transformer == null) {
2468
+ return operand.value;
2469
+ }
2470
+ if (operand.kind === "param" && operand.transformer == null) {
2471
+ this.structurallyDependsOnParams = true;
2472
+ return resolveParamPath(this.paramsName ?? "params", operand.path, this.params);
2473
+ }
2474
+ return UNKNOWN_UNTIL_ROW;
2475
+ }
1154
2476
  buildStandalone(operand) {
1155
2477
  if (operand.kind === "method-call") {
1156
2478
  return this.buildMethodComparator(operand);
@@ -1173,6 +2495,21 @@ const resolveParamPath = (paramsName, path, data)=>{
1173
2495
  locale: null
1174
2496
  }, /* applyConverter */ true);
1175
2497
  }
2498
+ // A boolean-valued call standing alone IS the predicate
2499
+ if (operand.kind === "arithmetic" && operand.call === "matches") {
2500
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2501
+ comparator: "equals",
2502
+ negated: false,
2503
+ strict: false,
2504
+ left: this.createOperandExpression(operand),
2505
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2506
+ value: true
2507
+ })
2508
+ });
2509
+ }
2510
+ if (operand.kind === "arithmetic" || operand.kind === "conditional") {
2511
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic used as a condition rather than compared"));
2512
+ }
1176
2513
  // Constant `true` — a tautology, which parseAnd/parseOr simplify away
1177
2514
  if (operand.kind === "value" && operand.value === true && operand.transformer == null) {
1178
2515
  return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
@@ -1225,12 +2562,6 @@ const resolveParamPath = (paramsName, path, data)=>{
1225
2562
  right: this.createValueExpression(value, null, /* applyConverter */ false)
1226
2563
  });
1227
2564
  }
1228
- // Casing transformers on a property are only meaningful with string-matching
1229
- // comparators; on relational comparators the plugins would silently
1230
- // ignore them and return wrong data
1231
- if (property.transformer != null && !isStringMatch) {
1232
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
1233
- }
1234
2565
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1235
2566
  comparator: operator.comparator,
1236
2567
  negated: operator.negated,
@@ -1239,31 +2570,60 @@ const resolveParamPath = (paramsName, path, data)=>{
1239
2570
  right: this.createValueExpression(value, property.property, applyConverter)
1240
2571
  });
1241
2572
  }
2573
+ /**
2574
+ * Any operand as an expression.
2575
+ *
2576
+ * Values inside arithmetic take no paired property: the result is a computed number, so the
2577
+ * property's serializer and type converter do not describe it — the same reason `.length` skips
2578
+ * them.
2579
+ */ createOperandExpression(operand) {
2580
+ if (operand.kind === "conditional") {
2581
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2582
+ call: "conditional",
2583
+ expression: operand.condition,
2584
+ arguments: [
2585
+ this.createOperandExpression(operand.whenTrue),
2586
+ this.createOperandExpression(operand.whenFalse)
2587
+ ]
2588
+ });
2589
+ }
2590
+ if (operand.kind === "arithmetic") {
2591
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2592
+ call: operand.call,
2593
+ expression: this.createOperandExpression(operand.left),
2594
+ arguments: operand.right === NO_ARGUMENT ? [] : operand.extra == null ? [
2595
+ this.createOperandExpression(operand.right)
2596
+ ] : [
2597
+ this.createOperandExpression(operand.right),
2598
+ this.createOperandExpression(operand.extra)
2599
+ ]
2600
+ });
2601
+ }
2602
+ if (operand.kind === "property") {
2603
+ return this.createPropertyExpression(operand);
2604
+ }
2605
+ if (operand.kind === "method-call") {
2606
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a method call inside arithmetic"));
2607
+ }
2608
+ return this.createValueExpression(operand, null, /* applyConverter */ false);
2609
+ }
1242
2610
  createPropertyExpression(operand) {
1243
- const expression = new _types__rspack_import_1/* .PropertyExpression */.ep({
2611
+ return asCall(new _types__rspack_import_1/* .PropertyExpression */.ep({
1244
2612
  property: operand.property
1245
- });
1246
- expression.transformer = operand.transformer;
1247
- expression.locale = operand.locale;
1248
- return expression;
2613
+ }), operand.transformer, operand.locale);
1249
2614
  }
1250
2615
  createValueExpression(operand, pairedProperty, applyConverter) {
1251
2616
  if (operand.kind === "param") {
1252
- const expression = new ParamReferenceExpression({
2617
+ return asCall(new ParamReferenceExpression({
1253
2618
  paramPath: operand.path,
1254
2619
  pairedProperty,
1255
2620
  applyConverter
1256
- });
1257
- expression.transformer = operand.transformer;
1258
- expression.locale = operand.locale;
1259
- return expression;
2621
+ }), operand.transformer, operand.locale);
1260
2622
  }
1261
2623
  const expression = new _types__rspack_import_1/* .ValueExpression */.Ko({
1262
2624
  value: resolvePairedValue(operand.value, pairedProperty, applyConverter)
1263
2625
  });
1264
- expression.transformer = operand.transformer;
1265
- expression.locale = operand.locale;
1266
- return expression;
2626
+ return asCall(expression, operand.transformer, operand.locale);
1267
2627
  }
1268
2628
  }
1269
2629
  // #endregion
@@ -1275,28 +2635,19 @@ const resolveParamPath = (paramsName, path, data)=>{
1275
2635
  */ const bindExpression = (expression, paramsName, params)=>{
1276
2636
  if (expression instanceof ParamReferenceExpression) {
1277
2637
  const raw = resolveParamPath(paramsName ?? "params", expression.paramPath, params);
1278
- const bound = new _types__rspack_import_1/* .ValueExpression */.Ko({
2638
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1279
2639
  value: resolvePairedValue(raw, expression.pairedProperty, expression.applyConverter)
1280
2640
  });
1281
- bound.transformer = expression.transformer;
1282
- bound.locale = expression.locale;
1283
- return bound;
1284
2641
  }
1285
2642
  if (expression instanceof _types__rspack_import_1/* .ValueExpression */.Ko) {
1286
- const clone = new _types__rspack_import_1/* .ValueExpression */.Ko({
2643
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1287
2644
  value: expression.value
1288
2645
  });
1289
- clone.transformer = expression.transformer;
1290
- clone.locale = expression.locale;
1291
- return clone;
1292
2646
  }
1293
2647
  if (expression instanceof _types__rspack_import_1/* .PropertyExpression */.ep) {
1294
- const clone = new _types__rspack_import_1/* .PropertyExpression */.ep({
2648
+ return new _types__rspack_import_1/* .PropertyExpression */.ep({
1295
2649
  property: expression.property
1296
2650
  });
1297
- clone.transformer = expression.transformer;
1298
- clone.locale = expression.locale;
1299
- return clone;
1300
2651
  }
1301
2652
  if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
1302
2653
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
@@ -1314,8 +2665,99 @@ const resolveParamPath = (paramsName, path, data)=>{
1314
2665
  right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
1315
2666
  });
1316
2667
  }
2668
+ if (expression instanceof _types__rspack_import_1/* .CallExpression */.DG) {
2669
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2670
+ call: expression.call,
2671
+ expression: bindExpression(expression.expression, paramsName, params),
2672
+ arguments: expression.arguments.map((argument)=>bindExpression(argument, paramsName, params))
2673
+ });
2674
+ }
1317
2675
  return expression;
1318
2676
  };
2677
+ /**
2678
+ * Wraps an operand in the call a transform method named, if there was one.
2679
+ *
2680
+ * `Transformer` and `Call` share these three names, so the transform IS the call name. A locale
2681
+ * becomes the call's first argument, which is where it belongs — it qualifies the casing, not the
2682
+ * property.
2683
+ */ const asCall = (inner, transformer, locale)=>{
2684
+ if (transformer == null) {
2685
+ return inner;
2686
+ }
2687
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2688
+ call: transformer,
2689
+ expression: inner,
2690
+ arguments: locale == null ? [] : [
2691
+ new _types__rspack_import_1/* .ValueExpression */.Ko({
2692
+ value: locale
2693
+ })
2694
+ ]
2695
+ });
2696
+ };
2697
+ /** Binds every name a destructuring pattern introduces to the path it reads. */ const bindPattern = (stream, kind, path, scope)=>{
2698
+ if (!stream.matchPunctuation("{")) {
2699
+ const name = stream.next();
2700
+ if (name.kind !== "identifier") {
2701
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`parameter '${name.value}'`));
2702
+ }
2703
+ scope.set(name.value, {
2704
+ kind,
2705
+ path
2706
+ });
2707
+ return;
2708
+ }
2709
+ while(!stream.matchPunctuation("}")){
2710
+ const key = stream.next();
2711
+ if (key.kind !== "identifier") {
2712
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`destructured key '${key.value}'`));
2713
+ }
2714
+ if (stream.matchPunctuation(":")) {
2715
+ bindPattern(stream, kind, [
2716
+ ...path,
2717
+ key.value
2718
+ ], scope);
2719
+ } else {
2720
+ scope.set(key.value, {
2721
+ kind,
2722
+ path: [
2723
+ ...path,
2724
+ key.value
2725
+ ]
2726
+ });
2727
+ }
2728
+ if (!stream.matchPunctuation(",")) {
2729
+ stream.expectPunctuation("}");
2730
+ return;
2731
+ }
2732
+ }
2733
+ };
2734
+ /** Reads a filter's parameter list — the entity alone, or the `[entity, params]` pair — into a scope. */ const buildScope = (parameterNames, hasParams)=>{
2735
+ const stream = new TokenStream(tokenize(parameterNames));
2736
+ const scope = new Map();
2737
+ if (!stream.matchPunctuation("[")) {
2738
+ bindPattern(stream, "property", [], scope);
2739
+ return {
2740
+ scope,
2741
+ paramsName: null
2742
+ };
2743
+ }
2744
+ bindPattern(stream, "property", [], scope);
2745
+ if (hasParams && stream.matchPunctuation(",") && !stream.isPunctuation("]")) {
2746
+ bindPattern(stream, "param", [], scope);
2747
+ }
2748
+ return {
2749
+ scope,
2750
+ paramsName: wholeParamsName(scope)
2751
+ };
2752
+ };
2753
+ /** The name the whole params object was given, when it was not destructured. Error messages only. */ const wholeParamsName = (scope)=>{
2754
+ for (const [name, binding] of scope){
2755
+ if (binding.kind === "param" && binding.path.length === 0) {
2756
+ return name;
2757
+ }
2758
+ }
2759
+ return null;
2760
+ };
1319
2761
  /**
1320
2762
  * Splits stringified filter source into parameter names and the expression
1321
2763
  * body, unwrapping single-return block bodies.
@@ -1345,33 +2787,12 @@ const resolveParamPath = (paramsName, path, data)=>{
1345
2787
  parameterNames = parameterNames.slice(1, -1).trim();
1346
2788
  }
1347
2789
  }
1348
- let entityName;
1349
- let paramsName = null;
1350
- if (parameterNames.startsWith("[") && parameterNames.endsWith("]")) {
1351
- const destructured = parameterNames.slice(1, -1).split(",").map((w)=>w.trim());
1352
- entityName = destructured[0];
1353
- if (hasParams) {
1354
- paramsName = destructured[1] ?? null;
1355
- }
1356
- } else {
1357
- entityName = parameterNames;
1358
- }
1359
- if (entityName == null || entityName.length === 0) {
2790
+ if (parameterNames.length === 0) {
1360
2791
  throw new Error("Invalid Function");
1361
2792
  }
1362
- // Unwrap a single-return block body: { return <expression>; }
1363
- if (body.startsWith("{")) {
1364
- const inner = body.slice(1, body.lastIndexOf("}")).trim();
1365
- if (!inner.startsWith("return")) {
1366
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("block body without a single return statement"));
1367
- }
1368
- body = inner.slice("return".length).trim();
1369
- if (body.endsWith(";")) {
1370
- body = body.slice(0, -1).trim();
1371
- }
1372
- }
2793
+ const { scope, paramsName } = buildScope(parameterNames, hasParams);
1373
2794
  return {
1374
- entityName,
2795
+ scope,
1375
2796
  paramsName,
1376
2797
  body
1377
2798
  };
@@ -1439,17 +2860,27 @@ const combineExpressions = (...expressions)=>{
1439
2860
  */ const parseFragment = (schema, body, rootName)=>{
1440
2861
  try {
1441
2862
  const stream = new TokenStream(tokenize(body));
1442
- const parser = new ExpressionParser(schema, stream, rootName, null, undefined);
1443
- return parser.parse();
2863
+ const scope = new Map([
2864
+ [
2865
+ rootName,
2866
+ {
2867
+ kind: "property",
2868
+ path: []
2869
+ }
2870
+ ]
2871
+ ]);
2872
+ const parser = new ExpressionParser(schema, stream, scope, null, undefined);
2873
+ return (0,_fold__rspack_import_4/* .foldConstantCalls */.F5)(parser.parse());
1444
2874
  } catch {
1445
2875
  // The failure is expected and informative — see above — so it is not logged. A caller that
1446
2876
  // parses one conjunct against two schemas would otherwise warn on every successful split.
1447
2877
  return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
1448
2878
  }
1449
2879
  };
2880
+ /** What the parser refused, from a throw that may not be an `Error`. */ const refusalOf = (error)=>error instanceof Error ? error.message : String(error);
1450
2881
  const toExpression = (schema, fn, params)=>{
1451
2882
  const stringifiedFunction = fn.toString();
1452
- const warn = (error)=>_utilities__rspack_import_3/* .logger.warn */.vF.warn("Error parsing expression", {
2883
+ const warn = (error)=>_utilities__rspack_import_5/* .logger.warn */.vF.warn("Error parsing expression", {
1453
2884
  error,
1454
2885
  collectionName: schema.collectionName,
1455
2886
  params,
@@ -1457,16 +2888,17 @@ const toExpression = (schema, fn, params)=>{
1457
2888
  });
1458
2889
  const cached = getCachedTemplate(schema, stringifiedFunction);
1459
2890
  if (cached != null) {
1460
- // A cached failure — the warning was already logged when it was discovered
2891
+ // A cached failure — the warning was already logged when it was discovered. The template
2892
+ // carries what was refused, and `.explain()` is usually called once the cache is warm.
1461
2893
  if (_types__rspack_import_1/* .Expression.isNotParsable */.r4.isNotParsable(cached.template)) {
1462
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
2894
+ return cached.template;
1463
2895
  }
1464
2896
  try {
1465
- return bindExpression(cached.template, cached.paramsName, params);
2897
+ return (0,_fold__rspack_import_4/* .foldConstantCalls */.F5)(bindExpression(cached.template, cached.paramsName, params));
1466
2898
  } catch (error) {
1467
2899
  // Binding failures are param-dependent by nature — never cached
1468
2900
  warn(error);
1469
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
2901
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1470
2902
  }
1471
2903
  }
1472
2904
  let paramsName = null;
@@ -1475,22 +2907,23 @@ const toExpression = (schema, fn, params)=>{
1475
2907
  try {
1476
2908
  const shape = resolveFunctionShape(stringifiedFunction, params != null);
1477
2909
  const stream = new TokenStream(tokenize(shape.body));
1478
- const parser = new ExpressionParser(schema, stream, shape.entityName, shape.paramsName, params);
2910
+ const parser = new ExpressionParser(schema, stream, shape.scope, shape.paramsName, params);
1479
2911
  paramsName = shape.paramsName;
1480
- template = parser.parse();
2912
+ template = parser.parseBody();
1481
2913
  structurallyDependsOnParams = parser.structurallyDependsOnParams;
1482
2914
  } catch (error) {
1483
2915
  // Cache the failure so a hot query on an unsupported filter doesn't
1484
2916
  // re-parse and re-warn on every execution. Param-dependent failures are
1485
2917
  // exempt: the same source can succeed with different params.
2918
+ const refused = _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1486
2919
  if (!(error instanceof ParamDependentParseError)) {
1487
2920
  setCachedTemplate(schema, stringifiedFunction, {
1488
- template: _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE,
2921
+ template: refused,
1489
2922
  paramsName: null
1490
2923
  });
1491
2924
  }
1492
2925
  warn(error);
1493
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
2926
+ return refused;
1494
2927
  }
1495
2928
  // Templates whose structure was resolved from param values are only
1496
2929
  // valid for this exact params object — parse those fresh every time
@@ -1501,10 +2934,10 @@ const toExpression = (schema, fn, params)=>{
1501
2934
  });
1502
2935
  }
1503
2936
  try {
1504
- return bindExpression(template, paramsName, params);
2937
+ return (0,_fold__rspack_import_4/* .foldConstantCalls */.F5)(bindExpression(template, paramsName, params));
1505
2938
  } catch (error) {
1506
2939
  warn(error);
1507
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
2940
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1508
2941
  }
1509
2942
  };
1510
2943
 
@@ -1512,6 +2945,7 @@ const toExpression = (schema, fn, params)=>{
1512
2945
  },
1513
2946
  27(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1514
2947
  __webpack_require__.d(__webpack_exports__, {
2948
+ DG: () => (CallExpression),
1515
2949
  Ko: () => (ValueExpression),
1516
2950
  SC: () => (NotParsableExpression),
1517
2951
  Sm: () => (EmptyExpression),
@@ -1523,59 +2957,69 @@ __webpack_require__.d(__webpack_exports__, {
1523
2957
  const valueToJson = (value)=>{
1524
2958
  if (value === undefined) {
1525
2959
  return {
1526
- k: "undefined"
2960
+ undefined: true
1527
2961
  };
1528
2962
  }
1529
2963
  if (value === null) {
1530
- return {
1531
- k: "raw",
1532
- v: null
1533
- };
2964
+ return null;
1534
2965
  }
1535
2966
  if (value instanceof Date) {
1536
2967
  // ISO rather than epoch millis: it survives a human reading the payload, and an invalid
1537
2968
  // Date has no ISO form — so it is caught here rather than becoming a silent `null`.
1538
2969
  return {
1539
- k: "date",
1540
- v: value.toISOString()
2970
+ date: value.toISOString()
1541
2971
  };
1542
2972
  }
1543
2973
  if (Array.isArray(value)) {
1544
- return {
1545
- k: "array",
1546
- v: value.map(valueToJson)
1547
- };
2974
+ return value.map(valueToJson);
1548
2975
  }
1549
2976
  if (typeof value === "number" && Number.isFinite(value) === false) {
1550
2977
  // `JSON.stringify` turns all three of these into `null`, which would compare as a different
1551
2978
  // value entirely rather than failing.
1552
2979
  return {
1553
- k: "number",
1554
- v: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
2980
+ number: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
1555
2981
  };
1556
2982
  }
1557
2983
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2984
+ return value;
2985
+ }
2986
+ if (value instanceof RegExp) {
1558
2987
  return {
1559
- k: "raw",
1560
- v: value
2988
+ regex: {
2989
+ source: value.source,
2990
+ flags: value.flags
2991
+ }
2992
+ };
2993
+ }
2994
+ // `JSON.stringify` throws outright on a bigint rather than losing it quietly, so this is the one
2995
+ // tag that turns a crash into a value
2996
+ if (typeof value === "bigint") {
2997
+ return {
2998
+ bigint: value.toString()
1561
2999
  };
1562
3000
  }
1563
3001
  throw new Error(`Cannot serialize this filter value: only strings, numbers, booleans, null, undefined, Dates and arrays of those can cross a wire. ` + `Received: ${Object.prototype.toString.call(value)}`);
1564
3002
  };
1565
3003
  const valueFromJson = (value)=>{
1566
- if (value.k === "undefined") {
1567
- return undefined;
3004
+ if (value === null || typeof value !== "object") {
3005
+ return value;
3006
+ }
3007
+ if (Array.isArray(value)) {
3008
+ return value.map(valueFromJson);
1568
3009
  }
1569
- if (value.k === "date") {
1570
- return new Date(value.v);
3010
+ if ("date" in value) {
3011
+ return new Date(value.date);
1571
3012
  }
1572
- if (value.k === "array") {
1573
- return value.v.map(valueFromJson);
3013
+ if ("undefined" in value) {
3014
+ return undefined;
3015
+ }
3016
+ if ("regex" in value) {
3017
+ return new RegExp(value.regex.source, value.regex.flags);
1574
3018
  }
1575
- if (value.k === "number") {
1576
- return value.v === "NaN" ? Number.NaN : value.v === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
3019
+ if ("bigint" in value) {
3020
+ return BigInt(value.bigint);
1577
3021
  }
1578
- return value.v;
3022
+ return value.number === "NaN" ? Number.NaN : value.number === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1579
3023
  };
1580
3024
  /**
1581
3025
  * The base class for all expression types.
@@ -1592,6 +3036,9 @@ const valueFromJson = (value)=>{
1592
3036
  static get NOT_PARSABLE() {
1593
3037
  return new NotParsableExpression();
1594
3038
  }
3039
+ /** `NOT_PARSABLE`, carrying what the parser refused. */ static notParsable(reason) {
3040
+ return new NotParsableExpression(reason);
3041
+ }
1595
3042
  static isEmpty(expression) {
1596
3043
  return expression.type === "empty" || expression instanceof EmptyExpression;
1597
3044
  }
@@ -1608,7 +3055,7 @@ const valueFromJson = (value)=>{
1608
3055
  *
1609
3056
  * ## Why it is this small
1610
3057
  *
1611
- * Of the six node types a bound tree can contain, exactly one holds anything JSON cannot carry:
3058
+ * Of the seven node types a bound tree can contain, exactly one holds anything JSON cannot carry:
1612
3059
  * `PropertyExpression`, whose live `PropertyInfo` has functions, a parent chain and caches. It
1613
3060
  * reduces to a property PATH — `PropertyInfo.id` IS the dotted path, and `getProperty` is keyed by
1614
3061
  * exactly that — so rebinding is one lookup.
@@ -1623,7 +3070,7 @@ const valueFromJson = (value)=>{
1623
3070
  if (expression.type === "operator") {
1624
3071
  const operator = expression;
1625
3072
  return {
1626
- t: "operator",
3073
+ type: "operator",
1627
3074
  operator: operator.operator,
1628
3075
  ...operator.left != null && {
1629
3076
  left: Expression.toJson(operator.left)
@@ -1636,7 +3083,7 @@ const valueFromJson = (value)=>{
1636
3083
  if (expression.type === "comparator") {
1637
3084
  const comparator = expression;
1638
3085
  return {
1639
- t: "comparator",
3086
+ type: "comparator",
1640
3087
  comparator: comparator.comparator,
1641
3088
  negated: comparator.negated,
1642
3089
  strict: comparator.strict,
@@ -1648,29 +3095,41 @@ const valueFromJson = (value)=>{
1648
3095
  }
1649
3096
  };
1650
3097
  }
3098
+ if (expression.type === "call") {
3099
+ const call = expression;
3100
+ return {
3101
+ type: "call",
3102
+ call: call.call,
3103
+ expression: Expression.toJson(call.expression),
3104
+ arguments: call.arguments.map(Expression.toJson)
3105
+ };
3106
+ }
1651
3107
  if (expression.type === "property") {
1652
3108
  const property = expression;
1653
3109
  return {
1654
- t: "property",
3110
+ type: "property",
1655
3111
  // The dotted path, which is exactly the key `getProperty` is looking up
1656
- path: property.property.id,
1657
- transformer: property.transformer,
1658
- locale: property.locale
3112
+ path: property.property.id
1659
3113
  };
1660
3114
  }
1661
3115
  if (expression.type === "value") {
1662
3116
  const value = expression;
1663
3117
  return {
1664
- t: "value",
1665
- value: valueToJson(value.value),
1666
- transformer: value.transformer,
1667
- locale: value.locale
3118
+ type: "value",
3119
+ value: valueToJson(value.value)
3120
+ };
3121
+ }
3122
+ if (expression.type === "empty") {
3123
+ return {
3124
+ type: "empty"
1668
3125
  };
1669
3126
  }
1670
- return expression.type === "empty" ? {
1671
- t: "empty"
3127
+ const reason = expression.reason;
3128
+ return reason == null ? {
3129
+ type: "not-parsable"
1672
3130
  } : {
1673
- t: "not-parsable"
3131
+ type: "not-parsable",
3132
+ reason
1674
3133
  };
1675
3134
  }
1676
3135
  /**
@@ -1686,14 +3145,14 @@ const valueFromJson = (value)=>{
1686
3145
  * failure here worse than an error.
1687
3146
  */ static fromJson(json, schema) {
1688
3147
  const child = (node)=>node == null ? undefined : Expression.fromJson(node, schema);
1689
- if (json.t === "operator") {
3148
+ if (json.type === "operator") {
1690
3149
  return new OperatorExpression({
1691
3150
  operator: json.operator,
1692
3151
  left: child(json.left),
1693
3152
  right: child(json.right)
1694
3153
  });
1695
3154
  }
1696
- if (json.t === "comparator") {
3155
+ if (json.type === "comparator") {
1697
3156
  return new ComparatorExpression({
1698
3157
  comparator: json.comparator,
1699
3158
  negated: json.negated,
@@ -1702,27 +3161,34 @@ const valueFromJson = (value)=>{
1702
3161
  right: child(json.right)
1703
3162
  });
1704
3163
  }
1705
- if (json.t === "property") {
3164
+ if (json.type === "call") {
3165
+ if (json.expression == null) {
3166
+ throw new Error(`Cannot deserialize a filter: a '${json.call}' call carries no operand. ` + `Collection: ${schema.collectionName}.`);
3167
+ }
3168
+ return new CallExpression({
3169
+ call: json.call,
3170
+ expression: Expression.fromJson(json.expression, schema),
3171
+ arguments: (json.arguments ?? []).map((argument)=>Expression.fromJson(argument, schema))
3172
+ });
3173
+ }
3174
+ if (json.type === "property") {
1706
3175
  const property = schema.getProperty(json.path);
1707
3176
  if (property == null) {
1708
3177
  throw new Error(`Cannot deserialize a filter: this schema does not declare the property it names. ` + `Property: ${json.path}, Collection: ${schema.collectionName}. ` + `The two sides disagree about the shape of the data, so the filter cannot be applied.`);
1709
3178
  }
1710
- const rebuilt = new PropertyExpression({
3179
+ return new PropertyExpression({
1711
3180
  property
1712
3181
  });
1713
- rebuilt.transformer = json.transformer;
1714
- rebuilt.locale = json.locale;
1715
- return rebuilt;
1716
3182
  }
1717
- if (json.t === "value") {
1718
- const rebuilt = new ValueExpression({
3183
+ if (json.type === "value") {
3184
+ return new ValueExpression({
1719
3185
  value: valueFromJson(json.value)
1720
3186
  });
1721
- rebuilt.transformer = json.transformer;
1722
- rebuilt.locale = json.locale;
1723
- return rebuilt;
1724
3187
  }
1725
- return json.t === "empty" ? Expression.EMPTY : Expression.NOT_PARSABLE;
3188
+ if (json.type === "empty") {
3189
+ return Expression.EMPTY;
3190
+ }
3191
+ return json.reason == null ? Expression.NOT_PARSABLE : Expression.notParsable(json.reason);
1726
3192
  }
1727
3193
  }
1728
3194
  class EmptyExpression extends Expression {
@@ -1730,6 +3196,11 @@ class EmptyExpression extends Expression {
1730
3196
  }
1731
3197
  class NotParsableExpression extends Expression {
1732
3198
  type = "not-parsable";
3199
+ /** What the parser refused, when it knows. `.explain()` prints it beside the source. */ reason;
3200
+ constructor(reason){
3201
+ super();
3202
+ this.reason = reason;
3203
+ }
1733
3204
  }
1734
3205
  /**
1735
3206
  * A class representing a comparison operation (e.g., equals, greater-than).
@@ -1760,20 +3231,28 @@ class NotParsableExpression extends Expression {
1760
3231
  */ class PropertyExpression extends Expression {
1761
3232
  /** The type of the expression (always 'property'). */ type = "property";
1762
3233
  /** The property info for the path. */ property;
1763
- transformer = null;
1764
- locale = null;
1765
3234
  constructor(options){
1766
3235
  super();
1767
3236
  this.property = options.property;
1768
3237
  }
1769
3238
  }
3239
+ class CallExpression extends Expression {
3240
+ type = "call";
3241
+ call;
3242
+ expression;
3243
+ /** Empty for a unary call. */ arguments;
3244
+ constructor(options){
3245
+ super();
3246
+ this.call = options.call;
3247
+ this.expression = options.expression;
3248
+ this.arguments = options.arguments ?? [];
3249
+ }
3250
+ }
1770
3251
  /**
1771
3252
  * A class representing a literal value.
1772
3253
  */ class ValueExpression extends Expression {
1773
3254
  /** The type of the expression (always 'value'). */ type = "value";
1774
3255
  /** The literal value. */ value;
1775
- transformer = null;
1776
- locale = null;
1777
3256
  constructor(options){
1778
3257
  super();
1779
3258
  this.value = options.value;
@@ -1784,9 +3263,45 @@ class NotParsableExpression extends Expression {
1784
3263
  },
1785
3264
  63(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1786
3265
  __webpack_require__.d(__webpack_exports__, {
1787
- j: () => (forEach),
1788
- o: () => (getProperties)
3266
+ CC: () => (peelCalls),
3267
+ LU: () => (childrenOf),
3268
+ jJ: () => (forEach),
3269
+ oY: () => (getProperties)
1789
3270
  });
3271
+ /**
3272
+ * Separates an operand from the calls applied to it.
3273
+ *
3274
+ * `null` when there is no operand beneath the calls. Every consumer needs this to decide whether a
3275
+ * comparator side is a property or a value, so it lives here rather than in each translator.
3276
+ */ function peelCalls(expression) {
3277
+ const calls = [];
3278
+ let current = expression;
3279
+ while(current != null && current.type === "call"){
3280
+ calls.unshift(current);
3281
+ current = current.expression;
3282
+ }
3283
+ return current == null ? null : {
3284
+ operand: current,
3285
+ calls
3286
+ };
3287
+ }
3288
+ function childrenOf(expression) {
3289
+ if (expression.type === "call") {
3290
+ const call = expression;
3291
+ return [
3292
+ call.expression,
3293
+ ...call.arguments ?? []
3294
+ ].filter((child)=>child != null);
3295
+ }
3296
+ const children = [];
3297
+ if (expression.left != null) {
3298
+ children.push(expression.left);
3299
+ }
3300
+ if (expression.right != null) {
3301
+ children.push(expression.right);
3302
+ }
3303
+ return children;
3304
+ }
1790
3305
  /**
1791
3306
  * Extracts all properties referenced in an expression
1792
3307
  * @param expression The expression to analyze
@@ -1798,12 +3313,8 @@ __webpack_require__.d(__webpack_exports__, {
1798
3313
  if (expr.type === "property") {
1799
3314
  properties.push(expr.property);
1800
3315
  }
1801
- // Traverse left and right expressions if they exist
1802
- if (expr.left) {
1803
- traverse(expr.left);
1804
- }
1805
- if (expr.right) {
1806
- traverse(expr.right);
3316
+ for (const child of childrenOf(expr)){
3317
+ traverse(child);
1807
3318
  }
1808
3319
  }
1809
3320
  traverse(expression);
@@ -1816,14 +3327,8 @@ function forEach(expression, callback) {
1816
3327
  if (!callback(expr)) {
1817
3328
  return false;
1818
3329
  }
1819
- // Traverse left and right expressions if they exist
1820
- if (expr.left) {
1821
- if (!traverse(expr.left)) {
1822
- return false;
1823
- }
1824
- }
1825
- if (expr.right) {
1826
- if (!traverse(expr.right)) {
3330
+ for (const child of childrenOf(expr)){
3331
+ if (!traverse(child)) {
1827
3332
  return false;
1828
3333
  }
1829
3334
  }
@@ -1943,12 +3448,14 @@ const isLogLevel = (value)=>typeof value === 'string' && LOG_LEVELS.includes(val
1943
3448
  const debug = process.env.DEBUG;
1944
3449
  if (debug === 'routier' || debug === '*') return 'debug';
1945
3450
  const env = "production"?.toLowerCase();
1946
- // `test` is deliberately absent. It used to be here, which meant no test suite anywhere
1947
- // could run Routier quietly. Opt in with DEBUG=routier or ROUTIER_LOG_LEVEL when a test
1948
- // needs the output.
1949
3451
  if (env === 'dev' || env === 'development') return 'debug';
1950
3452
  }
1951
- return 'silent';
3453
+ // Warnings are on unless something turns them off.
3454
+ //
3455
+ // Routier warns when a query returns correct rows a slower way than it could, or when a filter
3456
+ // compares types that can never match. Both are the caller's to act on, and a default of
3457
+ // `silent` meant the only people who ever saw them were the ones who already knew to look.
3458
+ return 'warn';
1952
3459
  };
1953
3460
  let level = resolveLevel();
1954
3461
  let rank = RANK[level];
@@ -2051,28 +3558,42 @@ var __webpack_exports__ = {};
2051
3558
  (() => {
2052
3559
  __webpack_require__.r(__webpack_exports__);
2053
3560
  __webpack_require__.d(__webpack_exports__, {
2054
- ComparatorExpression: () => (/* reexport safe */ _types__rspack_import_2.bQ),
2055
- EXPRESSION_TYPES: () => (/* reexport safe */ _constants__rspack_import_4.t),
2056
- EmptyExpression: () => (/* reexport safe */ _types__rspack_import_2.Sm),
2057
- Expression: () => (/* reexport safe */ _types__rspack_import_2.r4),
2058
- NotParsableExpression: () => (/* reexport safe */ _types__rspack_import_2.SC),
2059
- OperatorExpression: () => (/* reexport safe */ _types__rspack_import_2.fw),
2060
- PropertyExpression: () => (/* reexport safe */ _types__rspack_import_2.ep),
2061
- ValueExpression: () => (/* reexport safe */ _types__rspack_import_2.Ko),
2062
- combineExpressions: () => (/* reexport safe */ _parser__rspack_import_1.pg),
2063
- evaluate: () => (/* reexport safe */ _evaluate__rspack_import_0._3),
2064
- forEach: () => (/* reexport safe */ _utils__rspack_import_3.j),
2065
- getProperties: () => (/* reexport safe */ _utils__rspack_import_3.o),
2066
- parseFragment: () => (/* reexport safe */ _parser__rspack_import_1.oH),
2067
- toExpression: () => (/* reexport safe */ _parser__rspack_import_1.MY),
2068
- toPredicate: () => (/* reexport safe */ _evaluate__rspack_import_0.Vu),
2069
- toStrictPredicate: () => (/* reexport safe */ _evaluate__rspack_import_0.wS)
3561
+ CALL_SOURCE: () => (/* reexport safe */ _callSource__rspack_import_0.N),
3562
+ CallExpression: () => (/* reexport safe */ _types__rspack_import_4.DG),
3563
+ ComparatorExpression: () => (/* reexport safe */ _types__rspack_import_4.bQ),
3564
+ EXPRESSION_TYPES: () => (/* reexport safe */ _constants__rspack_import_6.t),
3565
+ EmptyExpression: () => (/* reexport safe */ _types__rspack_import_4.Sm),
3566
+ Expression: () => (/* reexport safe */ _types__rspack_import_4.r4),
3567
+ FOLDABLE: () => (/* reexport safe */ _fold__rspack_import_2.Sv),
3568
+ NotParsableExpression: () => (/* reexport safe */ _types__rspack_import_4.SC),
3569
+ OperatorExpression: () => (/* reexport safe */ _types__rspack_import_4.fw),
3570
+ PropertyExpression: () => (/* reexport safe */ _types__rspack_import_4.ep),
3571
+ UNRESOLVED: () => (/* reexport safe */ _evaluate__rspack_import_1.gm),
3572
+ ValueExpression: () => (/* reexport safe */ _types__rspack_import_4.Ko),
3573
+ childrenOf: () => (/* reexport safe */ _utils__rspack_import_5.LU),
3574
+ combineExpressions: () => (/* reexport safe */ _parser__rspack_import_3.pg),
3575
+ evaluate: () => (/* reexport safe */ _evaluate__rspack_import_1._3),
3576
+ foldConstantCalls: () => (/* reexport safe */ _fold__rspack_import_2.F5),
3577
+ foldedOperandValue: () => (/* reexport safe */ _fold__rspack_import_2.br),
3578
+ forEach: () => (/* reexport safe */ _utils__rspack_import_5.jJ),
3579
+ getProperties: () => (/* reexport safe */ _utils__rspack_import_5.oY),
3580
+ operandValue: () => (/* reexport safe */ _evaluate__rspack_import_1.Vv),
3581
+ parseFragment: () => (/* reexport safe */ _parser__rspack_import_3.oH),
3582
+ peelCalls: () => (/* reexport safe */ _utils__rspack_import_5.CC),
3583
+ renderCallAsJs: () => (/* reexport safe */ _callSource__rspack_import_0.a),
3584
+ toExpression: () => (/* reexport safe */ _parser__rspack_import_3.MY),
3585
+ toPredicate: () => (/* reexport safe */ _evaluate__rspack_import_1.Vu),
3586
+ toStrictPredicate: () => (/* reexport safe */ _evaluate__rspack_import_1.wS)
2070
3587
  });
2071
- /* import */ var _evaluate__rspack_import_0 = __webpack_require__(379);
2072
- /* import */ var _parser__rspack_import_1 = __webpack_require__(91);
2073
- /* import */ var _types__rspack_import_2 = __webpack_require__(27);
2074
- /* import */ var _utils__rspack_import_3 = __webpack_require__(63);
2075
- /* import */ var _constants__rspack_import_4 = __webpack_require__(835);
3588
+ /* import */ var _callSource__rspack_import_0 = __webpack_require__(429);
3589
+ /* import */ var _evaluate__rspack_import_1 = __webpack_require__(379);
3590
+ /* import */ var _fold__rspack_import_2 = __webpack_require__(43);
3591
+ /* import */ var _parser__rspack_import_3 = __webpack_require__(91);
3592
+ /* import */ var _types__rspack_import_4 = __webpack_require__(27);
3593
+ /* import */ var _utils__rspack_import_5 = __webpack_require__(63);
3594
+ /* import */ var _constants__rspack_import_6 = __webpack_require__(835);
3595
+
3596
+
2076
3597
 
2077
3598
 
2078
3599