spg_engine/eval/compiled.rs
1//! Compiled expressions — PG's ExprState idea (cut 30, extracted
2//! from `eval.rs`; v7.32 perf knife D / architecture v2 P1).
3//!
4//! Walk the tree ONCE per query, pre-resolve column positions and
5//! collation-fold decisions (both row-invariant), emit a flat
6//! post-order step program; per-row evaluation is a linear loop —
7//! no tree dispatch, no name resolution, no collation lookups.
8//! Anything the compiler doesn't model becomes a `Step::Subtree`
9//! that calls the interpreter for that node, so values AND error
10//! behaviour stay bit-for-bit with `eval_expr` (invariant I3).
11
12use alloc::format;
13use alloc::vec::Vec;
14
15use spg_sql::ast::{BinOp, ColumnName, Expr, Literal, UnOp};
16use spg_storage::{Row, Value};
17
18use super::{
19 EvalContext, EvalError, apply_binary, apply_unary, column_collation, composite_eq, eval_expr,
20 like_match_str, literal_to_value,
21};
22
23pub(crate) enum Step {
24 /// Pre-resolved column read (position into the row).
25 Column(usize),
26 /// Pre-converted literal.
27 Lit(Value<'static>),
28 /// Pops rhs then lhs, pushes the op result. Eager both-sides
29 /// evaluation — same as the interpreter for every op EXCEPT the two
30 /// boolean connectives, which take `Connective` below.
31 Binary(BinOp),
32 /// v7.39 (round 621) — COALESCE and NULLIF as steps on the borrowed
33 /// stack, because they are control flow wearing a function's name.
34 ///
35 /// Through `Step::Function` each had to return `Value<'static>`, which
36 /// forces a clone of a borrowed text argument; and the coalesce arm also
37 /// built a `Vec<DataType>` EVERY row for the numeric widening that
38 /// `COALESCE(1, 2.5)` needs. Measured: `count(coalesce(s,'z'))` at 3.00
39 /// allocations a row, `count(nullif(s,'row1'))` at 2.00, their chain at
40 /// 5.00 — all of it for values that end up borrowed from the row anyway.
41 ///
42 /// On the stack, the chosen argument is handed back AS IS. The widening
43 /// survives by inspection: only when the non-null arguments carry MIXED
44 /// numeric-family types does the step fall to the owned function arm,
45 /// which still does what it always did — same answers, paid only by the
46 /// mixed shapes that need it.
47 Coalesce {
48 n_args: usize,
49 },
50 NullIf,
51 /// v7.39 (round 717) — GREATEST / LEAST. Through `Step::Function`
52 /// every row re-ran `apply_function_lower`'s name dispatch, and
53 /// "least" lives in the crowded five-letter probe chain — measured
54 /// +6 ms over "greatest" on the same 500k scan REGARDLESS of which
55 /// argument wins (the take-always and take-never shapes cost the
56 /// same, so the branch was never the tax; the name was). Uniform
57 /// same-type arguments compare in place off the stack; the mixed /
58 /// coercing / xid / MySQL-NULL shapes fall to the function arm,
59 /// which still does what it always did.
60 Extremum {
61 n_args: usize,
62 max: bool,
63 },
64 /// v7.39 (round 621) — `AND` / `OR`, short-circuiting.
65 ///
66 /// The VM is a stack machine, so both operands were pushed before the
67 /// `Binary` step could look at either: `WHERE x <> 0 AND 1/x > 0` divided
68 /// by zero on exactly the rows the guard exists to exclude. The
69 /// interpreter's arm was fixed first and this path still failed, which is
70 /// the second time a connective has been fixed in one evaluator and not
71 /// the other (round 346's MySQL reading was the first — its comment is
72 /// three screens down).
73 ///
74 /// Rather than turn the hottest loop in the engine into an indexed one
75 /// with jumps, the right operand is its OWN program, run only when the
76 /// left does not decide. Nesting depth is the AND-nesting depth of the
77 /// predicate.
78 Connective {
79 op: BinOp,
80 rhs: Vec<Step>,
81 },
82 /// Comparison whose operands referenced a CaseInsensitive
83 /// column: ASCII-fold Text operands first (decided at compile
84 /// time; the interpreter re-decides per row).
85 BinaryCi(BinOp),
86 Unary(UnOp),
87 IsNull {
88 negated: bool,
89 },
90 /// v7.39 (round 488) — the verdict of an all-`%` LIKE pattern:
91 /// matches every non-NULL operand, and is NULL for a NULL one.
92 ///
93 /// v7.36 collapsed this shape into `IsNull { negated: !negated }`,
94 /// which answers a three-valued question two-valued. `NULL NOT LIKE
95 /// '%'` came out TRUE where PG18 says NULL, so `WHERE s NOT LIKE '%'`
96 /// SELECTED the NULL row (PG selects nothing), and `SELECT s LIKE '%'`
97 /// printed `false` where PG prints NULL. Same collapse, three-valued.
98 AnyTextMatch {
99 negated: bool,
100 },
101 /// v7.32 (architecture v2, P1) — `needle [NOT] IN (literals…)`.
102 /// The membership SET is a COMPILE PRODUCT, not a runtime cache:
103 /// it lives in the step, so there is no "forgot to pass the
104 /// memo" failure mode (the round-25 18.7 s accident is now
105 /// unconstructable — see v7.32-executor-architecture-design.md
106 /// invariant I2). The needle is the preceding sub-program; this
107 /// step pops it. `fallback` is the whole InList node, used only
108 /// when the runtime needle family doesn't match the set
109 /// (e.g. Float needle vs Int set) — same escape the interpreter
110 /// takes, evaluated cold.
111 InSet {
112 set: crate::memoize::InListSet,
113 has_null: bool,
114 negated: bool,
115 fallback: Expr,
116 },
117 /// v7.32 (P1) — `text [NOT] [I]LIKE '<literal pattern>'`. The
118 /// pattern (and its lowercased form for ILIKE) is compiled once;
119 /// the step pops the text operand.
120 Like {
121 pattern: alloc::vec::Vec<char>,
122 negated: bool,
123 case_insensitive: bool,
124 },
125 /// v7.39 (perf — like_filter tied 1.04×) — unanchored substring
126 /// LIKE: `%[k×_]literal[m×_]%`. Instead of the generic matcher's
127 /// try-every-suffix backtracking (per-position `_`+literal walk),
128 /// scan with `str::find` (two-way, sublinear) over the literal and
129 /// verify the `k` leading / `m` trailing wildcard chars have room.
130 /// v7.39 (round 594) — `text ~ '<literal pattern>'` and its `~*` /
131 /// `regexp_like(...)` spellings. `regexp_like` parsed the pattern into a
132 /// tree for EVERY row: 500k rows cost 350 ms against PG18's 34.5, the
133 /// same 10x whichever way the match was spelled. The pattern is a
134 /// compile product here, exactly as `Step::Like`'s is — PG solves the
135 /// same problem with a cache; a compile product cannot be forgotten.
136 /// v7.39 (round 597) — `<expr> <op> ANY/ALL (<constant array>)`. The
137 /// array is a compile PRODUCT: it used to be rebuilt for every row, and
138 /// `WHERE id = ANY (ARRAY[1..10])` cost 268 ms over 500k rows against
139 /// PG18's 8.3 — 494 at twenty elements — where the equivalent
140 /// `id IN (1..10)` took 2.3. A non-constant right-hand side keeps the
141 /// interpreter, which has to rebuild it: there it really can differ.
142 AnyAll {
143 op: spg_sql::ast::BinOp,
144 is_any: bool,
145 arr: Value<'static>,
146 },
147 /// v7.39 (round 595) — `EXTRACT(<field> FROM <expr>)`. The field is a
148 /// keyword, not a value, so it rides in the step; the source is the
149 /// preceding sub-program and this pops it. `fallback` carries the whole
150 /// node because the extraction's error wording names the source's
151 /// declared type, which only the node knows.
152 Extract {
153 field: spg_sql::ast::ExtractField,
154 fallback: Expr,
155 },
156 Regex {
157 re: crate::eval::CompiledRe,
158 /// The whole call, for an operand that is not text: the interpreter
159 /// owns whatever coercion or error that is, and this step must not
160 /// invent one. Same escape `Step::InSet` takes, evaluated cold.
161 fallback: Expr,
162 },
163 LikeSubstring {
164 needle: alloc::string::String,
165 k_before: usize,
166 m_after: usize,
167 negated: bool,
168 case_insensitive: bool,
169 },
170 /// v7.36 (perf — mailrs Ask 1) — pure scalar function call
171 /// (LENGTH, COALESCE, UPPER, etc.) on already-pushed args.
172 /// Pops `n_args` values, calls `apply_function(name, args, ctx)`,
173 /// pushes the result. Replaces the Subtree fallback for the
174 /// "function over bound columns" shape that aggregate arg paths
175 /// like `SUM(LENGTH(text_body))` and `MAX(COALESCE(col, ''))`
176 /// otherwise force the row-materialise eval path. Only the
177 /// `fully_compilable` whitelist (PURE scalars — no NOW / RANDOM
178 /// / sequence accessors) is emitted; everything else stays on
179 /// `Step::Subtree`.
180 /// `name_lower` is pre-lowercased at compile time so the per-
181 /// row dispatch in `apply_function` skips an allocation on
182 /// every input row.
183 Function {
184 name_lower: alloc::string::String,
185 n_args: usize,
186 },
187 /// v7.36 (perf — mailrs Ask 1 SUM(LENGTH(text_body)) zero-copy)
188 /// — `LENGTH(<column>)` / `CHAR_LENGTH(<column>)` /
189 /// `CHARACTER_LENGTH(<column>)` over a bound column. Reads the
190 /// cell by reference, computes the char length WITHOUT cloning
191 /// the underlying `String` — the 1 KB text bodies in
192 /// `user_storage_usage` otherwise pay 25 k × 1 KB heap allocs
193 /// per query just to push a `Value::Text` onto the stack so the
194 /// next Step pops it and asks `s.len()`.
195 ColumnLength {
196 pos: usize,
197 },
198 /// v7.36 — `OCTET_LENGTH(<column>)` — byte count, regardless of
199 /// encoding. Even simpler than `ColumnLength` (no ASCII probe).
200 ColumnOctetLength {
201 pos: usize,
202 },
203 /// v7.36 — `CAST(<expr> AS <ty>)` over an already-pushed value.
204 /// Pure / context-free conversion goes through the same
205 /// `cast_value` dispatcher the interpreter uses.
206 Cast {
207 target: spg_sql::ast::CastTarget,
208 },
209 /// v7.39 (round 722) — a NAMED cast whose name resolved at COMPILE
210 /// time (`::NUMERIC`, `::REAL`, `numeric(10,2)` — the
211 /// `plain_named_target` table). The blanket Named -> Subtree rule
212 /// sent these to the interpreter — worse, it made the whole
213 /// aggregate argument non-compilable, so `count(id::NUMERIC)` fell
214 /// off the round-716 fused parallel lane entirely. The name rides
215 /// along for error wording only.
216 CastPlain {
217 dt: spg_storage::DataType,
218 name: alloc::string::String,
219 },
220 /// v7.37.5-A2b — `CASE [operand] WHEN x THEN y … ELSE z END`.
221 /// Each `(when, then)` branch and the optional `else` is a
222 /// pre-compiled sub-program; the executor short-circuits on the
223 /// first matching WHEN. Compiles only when **every** sub-program
224 /// is itself `fully_compilable` (so the Case never falls back to
225 /// a Subtree that would force a row materialise — profile-guided
226 /// fix for Track A `COUNT(DISTINCT CASE WHEN ...)` aggregates).
227 /// Searched form has `operand=None` and treats each WHEN as a
228 /// Bool predicate; simple form has `operand=Some(prog)` and
229 /// compares the operand value with each WHEN via `BinOp::Eq`.
230 Case {
231 /// v7.38.14 — may the simple-form operand match fold text?
232 ///
233 /// Decided at COMPILE time, where the operand's and the WHEN
234 /// arms' expressions are still in hand, and never in the row
235 /// loop. The runtime arm below used `apply_binary_by_ref(Eq)`,
236 /// which is dialect-blind, so `CASE s WHEN 'A'` compared bytes
237 /// while the interpreter's `Expr::Case` folded — one semantic
238 /// with two implementations, disagreeing.
239 fold_operand: bool,
240 operand: Option<CompiledExpr>,
241 branches: alloc::vec::Vec<(CompiledExpr, CompiledExpr)>,
242 else_branch: Option<CompiledExpr>,
243 },
244 /// v7.38 (read01) — widen the top-of-stack value to a statically
245 /// resolved PG common type (e.g. a `CASE` whose branches mix integer and
246 /// numeric resolves to numeric). Resolved once at compile time from the
247 /// branch expressions' types, so the per-row cost is a single
248 /// scale-preserving coercion, not a describe. See
249 /// [`crate::eval::widen_value_to`].
250 CoerceCommon(spg_storage::DataType),
251 /// Fallback: interpret this subtree with eval_expr.
252 Subtree(Expr),
253}
254
255pub(crate) struct CompiledExpr {
256 steps: Vec<Step>,
257 /// Which fast predicate shape this program is — settled once, here,
258 /// instead of re-derived per row. See [`PredShape`].
259 pred_shape: PredShape,
260}
261
262/// v7.39 (round 486) — the shape of a compiled predicate, decided at
263/// compile time.
264///
265/// Round 482 added a `<column> <cmp> <literal>` fast path and this round
266/// added `<column> [NOT] IN (<literals>)`. Both were slice pattern-matches
267/// run PER ROW, so a program that is neither paid for every probe in the
268/// list: adding the second one cost `like_filter` — a shape with no `IN`
269/// anywhere in it — 4.5 %, measured against the previous commit on the same
270/// machine minutes apart. A program's shape does not change between its
271/// rows, so it is settled once and the row loop reads one discriminant.
272#[derive(Clone, Copy, PartialEq, Eq, Debug)]
273pub(crate) enum PredShape {
274 Other,
275 /// r1021 — an integer-only arithmetic predicate, run without building a
276 /// single `Value`. See [`CompiledExpr::is_int_arith_pred`].
277 IntArith,
278 ColumnCmpLit,
279 ColumnInSet,
280 ColumnLike,
281}
282
283impl CompiledExpr {
284 /// v7.36 (perf — mailrs Phase 1, user_storage_usage hot loop) —
285 /// shape inspector for the aggregate's tight inner. Returns
286 /// `Some(pos)` iff this compiled expression is exactly the
287 /// single step `ColumnLength { pos }` — i.e. `LENGTH(<column>)`
288 /// on a bound text column with no surrounding work.
289 /// r1021 — the deepest an integer lane will go. A predicate needing
290 /// more stack than this falls back; measured shapes use two or three.
291 const INT_LANE_DEPTH: usize = 8;
292
293 /// r1021 — is this predicate built only from integer columns, integer
294 /// literals and integer arithmetic, ending in one comparison?
295 ///
296 /// Round 482 traced the per-row predicate cost to `Value` churn and
297 /// answered it with ONE hard-coded shape, `<column> <cmp> <literal>`.
298 /// Anything with arithmetic in it — `id % 3 = 0`, the bucketing and
299 /// parity predicates real schemas are full of — still builds and
300 /// destroys a `Value` per step. Profiled (2026-08-14, see
301 /// `docs/PERF_FILTERED_THEN_ORDER_2026-08-14.md`):
302 /// `drop_glue<Value>` is the LARGEST leaf on `WHERE id % 3 = 0`, ahead
303 /// of the modulo it carries, and 22x heavier per rep than on the shape
304 /// that skips the step machine.
305 ///
306 /// So this recognises a CLASS rather than a shape. Structural only —
307 /// no column types are consulted here — because every value the lane
308 /// cannot handle makes it fall back at run time instead of guessing.
309 fn is_int_arith_pred(&self) -> bool {
310 let mut depth = 0usize;
311 let mut comparisons = 0usize;
312 for (i, step) in self.steps.iter().enumerate() {
313 match step {
314 Step::Column(_) => depth += 1,
315 Step::Lit(v) => {
316 if !matches!(v, Value::Int(_) | Value::BigInt(_)) {
317 return false;
318 }
319 depth += 1;
320 }
321 Step::Binary(op) => {
322 if depth < 2 {
323 return false;
324 }
325 depth -= 1;
326 if is_int_comparison(*op) {
327 comparisons += 1;
328 // The comparison is the answer, so it ends the
329 // program; a later step would consume a bool the
330 // lane does not carry.
331 if i + 1 != self.steps.len() {
332 return false;
333 }
334 } else if !is_int_arithmetic(*op) {
335 return false;
336 }
337 }
338 _ => return false,
339 }
340 if depth > Self::INT_LANE_DEPTH {
341 return false;
342 }
343 }
344 depth == 1 && comparisons == 1
345 }
346
347 /// r1021 — run an [`Self::is_int_arith_pred`] program over `i64`s.
348 ///
349 /// `None` means "this row is not for the lane" and the caller runs the
350 /// ordinary machine. Every case that could answer differently from the
351 /// interpreter takes that exit rather than deciding for itself: a NULL
352 /// or non-integer cell, a division by zero, an overflow, and a result
353 /// that would not fit the width the operands imply. The lane therefore
354 /// cannot change a single answer — it can only reach the same one
355 /// without a heap type in the middle.
356 ///
357 /// Width follows PG: `int4 op int4` stays `int4` and overflowing it is
358 /// an error, so a 32-bit result that leaves 32-bit range hands the row
359 /// back and the interpreter raises exactly as before. Mixed widths
360 /// widen to 64-bit, and `smallint` is simply not admitted.
361 fn eval_int_arith_pred(&self, row: &Row<'static>) -> Option<bool> {
362 let mut vals = [0i64; Self::INT_LANE_DEPTH];
363 let mut narrow = [false; Self::INT_LANE_DEPTH];
364 let mut n = 0usize;
365 for step in &self.steps {
366 match step {
367 Step::Column(pos) => {
368 let (v, is32) = int_operand(row.values.get(*pos)?)?;
369 vals[n] = v;
370 narrow[n] = is32;
371 n += 1;
372 }
373 Step::Lit(lit) => {
374 let (v, is32) = int_operand(lit)?;
375 vals[n] = v;
376 narrow[n] = is32;
377 n += 1;
378 }
379 Step::Binary(op) => {
380 let (rhs, rhs32) = (vals[n - 1], narrow[n - 1]);
381 let (lhs, lhs32) = (vals[n - 2], narrow[n - 2]);
382 n -= 2;
383 if is_int_comparison(*op) {
384 return Some(match op {
385 BinOp::Eq => lhs == rhs,
386 BinOp::NotEq => lhs != rhs,
387 BinOp::Lt => lhs < rhs,
388 BinOp::LtEq => lhs <= rhs,
389 BinOp::Gt => lhs > rhs,
390 _ => lhs >= rhs,
391 });
392 }
393 let out = match op {
394 BinOp::Add => lhs.checked_add(rhs)?,
395 BinOp::Sub => lhs.checked_sub(rhs)?,
396 BinOp::Mul => lhs.checked_mul(rhs)?,
397 BinOp::Div => lhs.checked_div(rhs)?,
398 _ => lhs.checked_rem(rhs)?,
399 };
400 let out32 = lhs32 && rhs32;
401 if out32 && i32::try_from(out).is_err() {
402 return None;
403 }
404 vals[n] = out;
405 narrow[n] = out32;
406 n += 1;
407 }
408 _ => return None,
409 }
410 }
411 None
412 }
413
414 /// v7.39 (round 482) — is this exactly `<column> <cmp> <literal>`?
415 ///
416 /// Rounds 478-481 traced the per-row predicate cost to `Value` churn:
417 /// three steps a row (Column, Lit, Binary) means three `Value`s built
418 /// and destroyed, and `drop_glue<Value>` is an out-of-line call that
419 /// switches on the discriminant even when the value carries no heap.
420 /// Round 481's counter ruled out leftovers on the stack — the churn is
421 /// the VM's ordinary operands.
422 ///
423 /// This shape needs none of them: both operands can be read by
424 /// reference. It covers `g = 5` and `s = '…'`; `LIKE` is its own AST
425 /// node rather than a `BinOp`, so it compiles to a different step and
426 /// is NOT covered here — measured, not assumed.
427 ///
428 /// `BinaryCi` is deliberately not matched: it folds its operands
429 /// first, which is a different comparison. Nor is the mirrored
430 /// `<literal> <cmp> <column>` — flipping the operator is a separate
431 /// judgement and this returns None so it takes the general path.
432 pub(crate) fn as_column_cmp_literal(&self) -> Option<(usize, BinOp, &Value<'static>)> {
433 let [Step::Column(pos), Step::Lit(lit), Step::Binary(op)] = &self.steps[..] else {
434 return None;
435 };
436 if !matches!(
437 op,
438 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
439 ) {
440 return None;
441 }
442 Some((*pos, *op, lit))
443 }
444
445 /// v7.39 (round 486) — the sibling shape `<column> [NOT] IN (<literals>)`.
446 ///
447 /// `big_in` is the read panel's worst shape and compiles to exactly two
448 /// steps, `Column` then `InSet`. The round-482 fast path does not cover
449 /// it (three steps, a `Binary`), so it runs the general VM: a `Value`
450 /// built from the cell, popped, and a `Value::Bool` built and popped
451 /// again. Its profile put `drop_glue<Value>` at 20 % and the VM loop at
452 /// 27 %. The set lookup itself wants nothing but a reference to the
453 /// cell.
454 pub(crate) fn as_column_in_set(
455 &self,
456 ) -> Option<(usize, &crate::memoize::InListSet, bool, bool)> {
457 let [
458 Step::Column(pos),
459 Step::InSet {
460 set,
461 has_null,
462 negated,
463 ..
464 },
465 ] = &self.steps[..]
466 else {
467 return None;
468 };
469 Some((*pos, set, *has_null, *negated))
470 }
471
472 /// v7.39 (round 488) — the third two-step shape: `<column> [NOT]
473 /// [I]LIKE '<literal>'`, in either the general matcher's form or the
474 /// unanchored-substring form round 484 added.
475 ///
476 /// `like_filter` is the read panel's worst shape. Rounds 482 and 486
477 /// covered its two siblings; this one still ran the general VM, which
478 /// pushes the cell as a `Value` and pops it again for a matcher that
479 /// only ever wanted a `&str`.
480 pub(crate) fn as_column_like(&self) -> Option<(usize, &Step)> {
481 let [
482 Step::Column(pos),
483 step @ (Step::Like { .. } | Step::LikeSubstring { .. }),
484 ] = &self.steps[..]
485 else {
486 return None;
487 };
488 Some((*pos, step))
489 }
490
491 pub(crate) fn as_single_column_length(&self) -> Option<usize> {
492 if self.steps.len() == 1
493 && let Step::ColumnLength { pos } = &self.steps[0]
494 {
495 Some(*pos)
496 } else {
497 None
498 }
499 }
500}
501
502/// Column-position resolution at compile time. Mirrors the happy
503/// layers of `resolve_column`; ANY case that would reach an error
504/// path, an ambiguity, or a miss returns None so the node falls
505/// back to the interpreter (identical runtime error / NULL
506/// semantics).
507///
508/// v7.37.16 — pub(crate): the aggregate bind-once fast path
509/// (aggregate.rs `col_pos`) uses this as its resolver so bare-name
510/// group/arg columns bind exactly like compiled-WHERE columns do.
511/// v7.39 (round 693) — does this comparison operand carry a collation the
512/// VM cannot perform?
513///
514/// Deliberately a COMPILE-time question. The answer is the same for every
515/// row of the scan, and the alternative — asking per row inside `compare` —
516/// puts a lookup on the hottest path in the engine.
517/// v7.39 (round 704) — does this comparison pair an unknown string literal
518/// with a numeric-family operand whose type the literal will not parse as?
519/// Compile-time twin of the eval Binary arm's error rewrite; see the bail
520/// site for why the shape cannot stay on the VM.
521fn unparseable_numeric_literal_cmp(lhs: &Expr, rhs: &Expr, ctx: &EvalContext<'_>) -> bool {
522 let check = |lit: &Expr, other: &Expr| -> bool {
523 let Expr::Literal(spg_sql::ast::Literal::String(text)) = lit else {
524 return false;
525 };
526 let Some(desc) = crate::describe::describe_expr(other, ctx.columns) else {
527 return false;
528 };
529 if !matches!(
530 desc.ty,
531 spg_storage::DataType::SmallInt
532 | spg_storage::DataType::Int
533 | spg_storage::DataType::BigInt
534 | spg_storage::DataType::Float
535 | spg_storage::DataType::Real
536 | spg_storage::DataType::Numeric { .. }
537 ) {
538 return false;
539 }
540 crate::conversions::coerce_value(spg_storage::Value::text(text.as_str()), desc.ty, "", 0)
541 .is_err()
542 };
543 check(lhs, rhs) || check(rhs, lhs)
544}
545
546fn operand_declares_a_collation(e: &Expr, ctx: &EvalContext<'_>) -> bool {
547 let derived = crate::collate_derive::derive(e, &|c: &ColumnName| {
548 let pos = crate::eval::find_column_pos(c, ctx)?;
549 ctx.columns.get(pos)?.collation_name.clone()
550 });
551 // A conflict has to leave the VM too — the tree evaluator is where the
552 // error is raised, with PG's own sentence.
553 derived.conflict().is_some()
554 || derived
555 .name()
556 .is_some_and(|n| crate::collate::is_supported(n))
557}
558
559pub(crate) fn compile_column_pos(c: &ColumnName, ctx: &EvalContext<'_>) -> Option<usize> {
560 if let Some(q) = &c.qualifier {
561 if let Some(pos) = ctx
562 .columns
563 .iter()
564 .position(|s| composite_eq(&s.name, q, &c.name))
565 {
566 return Some(pos);
567 }
568 // resolve_column's error layers live behind this point:
569 // composites under the qualifier exist (ColumnNotFound) or
570 // the qualifier is unknown (UnknownQualifier) — interpret.
571 let prefix_exists = ctx.columns.iter().any(|s| {
572 s.name.starts_with(q.as_str()) && s.name.as_bytes().get(q.len()) == Some(&b'.')
573 });
574 if prefix_exists {
575 return None;
576 }
577 match ctx.table_alias {
578 // Alias-accepted single-table reference: fall through
579 // to the bare layers (the inner-subquery hot shape).
580 Some(a) if a == q => {}
581 _ => return None,
582 }
583 }
584 if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
585 return Some(pos);
586 }
587 let mut matches = ctx.columns.iter().enumerate().filter(|(_, s)| {
588 s.name.len() > c.name.len()
589 && s.name.ends_with(c.name.as_str())
590 && s.name.as_bytes()[s.name.len() - c.name.len() - 1] == b'.'
591 });
592 let first = matches.next();
593 if matches.next().is_some() {
594 return None; // ambiguous — interpreter owns the error text
595 }
596 first.map(|(i, _)| i)
597}
598
599/// v7.39 (round 621) — can evaluating this raise at RUN time?
600///
601/// The errors a short circuit spares are the run-time ones: a division, an
602/// overflow, a cast that will not parse, a function that refuses its input.
603/// A type mismatch is not among them — PG raises those while ANALYSING, so it
604/// raises them whether or not the operand would have been evaluated, and so
605/// does SPG. That is why a predicate built only from columns, literals,
606/// comparisons and the boolean shapes over them needs no short circuit: there
607/// is nothing for it to spare.
608///
609/// Unrecognised shapes answer `true`, so a new kind of expression short
610/// circuits (correct, slightly slower) rather than silently not.
611fn can_raise_at_run_time(e: &Expr) -> bool {
612 match e {
613 Expr::Literal(_) | Expr::Column(_) => false,
614 Expr::Binary { op, lhs, rhs } => {
615 !matches!(
616 op,
617 BinOp::Eq
618 | BinOp::NotEq
619 | BinOp::Lt
620 | BinOp::LtEq
621 | BinOp::Gt
622 | BinOp::GtEq
623 | BinOp::And
624 | BinOp::Or
625 ) || can_raise_at_run_time(lhs)
626 || can_raise_at_run_time(rhs)
627 }
628 Expr::Unary { op, expr } => !matches!(op, UnOp::Not) || can_raise_at_run_time(expr),
629 Expr::IsNull { expr, .. } | Expr::BoolTest { expr, .. } => can_raise_at_run_time(expr),
630 Expr::Like { expr, pattern, .. } => {
631 can_raise_at_run_time(expr) || can_raise_at_run_time(pattern)
632 }
633 Expr::InList { expr, list, .. } => {
634 can_raise_at_run_time(expr) || list.iter().any(can_raise_at_run_time)
635 }
636 _ => true,
637 }
638}
639
640fn compile_into(e: &Expr, ctx: &EvalContext<'_>, steps: &mut Vec<Step>) {
641 match e {
642 Expr::Literal(l) => steps.push(Step::Lit(literal_to_value(l))),
643 Expr::Column(c) => match compile_column_pos(c, ctx) {
644 // v7.39 (read01 round 56) — a COMPOSITE column must not compile to
645 // a raw `Step::Column`: that loads the stored JSON straight off the
646 // row and skips the rehydration into `Value::Composite` that
647 // `resolve_column` does. `p = ROW(2,'b')::pt` in a WHERE then
648 // compared Json against Composite and errored, while the same
649 // predicate in a projection worked. Route it through eval instead.
650 // The check is COMPILE-time, so the hot column path pays nothing.
651 Some(pos)
652 if ctx
653 .columns
654 .get(pos)
655 .is_some_and(|sc| sc.user_composite_type.is_some()) =>
656 {
657 steps.push(Step::Subtree(e.clone()));
658 }
659 Some(pos) => steps.push(Step::Column(pos)),
660 None => steps.push(Step::Subtree(e.clone())),
661 },
662 Expr::Binary { lhs, op, rhs } => {
663 // v7.39 (round 383) — the MySQL bitwise operators are UNSIGNED
664 // 64-bit (`~ & | ^ << >>`); the VM's Step::Binary calls the
665 // dialect-blind apply_binary, so route them to the interpreter,
666 // which has the dialect (eval.rs `mysql_bitwise`). `<< >>` share
667 // the inet-containment BinOps — the interpreter still keeps the
668 // inet meaning for non-numeric operands.
669 if ctx.mysql_dialect
670 && matches!(
671 op,
672 BinOp::BitAnd
673 | BinOp::BitOr
674 | BinOp::BitXor
675 | BinOp::InetContainedBy
676 | BinOp::InetContains
677 )
678 {
679 steps.push(Step::Subtree(e.clone()));
680 return;
681 }
682 // v7.39 (round 407) — MySQL's logical `XOR` reads both sides as
683 // truth values, which the VM's dialect-blind apply_binary (no
684 // LogicalXor arm) cannot do. Route to the interpreter, whose
685 // eval_expr arm handles the connective (eval.rs
686 // `eval_mysql_connective`).
687 if ctx.mysql_dialect && matches!(op, BinOp::LogicalXor) {
688 steps.push(Step::Subtree(e.clone()));
689 return;
690 }
691 // v7.39 (round 402) — an arithmetic op on a SET / inline-ENUM
692 // column reads the column numerically (bitmask / 1-based
693 // ordinal), which the VM's value-level Add cannot see (it has the
694 // text). Route to the interpreter, which folds it (eval.rs
695 // resolve `collation_fold_for_compare`).
696 if ctx.mysql_dialect
697 && matches!(
698 op,
699 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
700 )
701 && (crate::eval::expr_set_variants(lhs, ctx.columns).is_some()
702 || crate::eval::expr_set_variants(rhs, ctx.columns).is_some()
703 || crate::eval::expr_inline_enum_variants(lhs, ctx.columns).is_some()
704 || crate::eval::expr_inline_enum_variants(rhs, ctx.columns).is_some())
705 {
706 steps.push(Step::Subtree(e.clone()));
707 return;
708 }
709 // v7.39 (round 621) — the boolean connectives short-circuit, so
710 // the right operand compiles to its own program. The shapes whose
711 // right operand is a literal go to the interpreter instead: those
712 // carry PG's analysis-time half (a non-boolean literal is refused
713 // even when the short circuit would not reach it, and an unknown
714 // string literal is resolved), which is decided there and is not
715 // worth a second implementation for how rare they are in a
716 // compiled predicate.
717 if matches!(op, BinOp::And | BinOp::Or) {
718 if matches!(rhs.as_ref(), Expr::Literal(_)) {
719 steps.push(Step::Subtree(e.clone()));
720 return;
721 }
722 // A right operand that cannot fail has nothing to be spared,
723 // so it keeps the eager step and its inline cost. `WHERE g
724 // BETWEEN 10 AND 20` is `g >= 10 AND g <= 20`, the commonest
725 // conjunctive predicate there is, and paying a nested program
726 // per row for it measured +42% to +60% on the panel — a real
727 // regression, reproduced, for a short circuit that can never
728 // change an answer.
729 if !can_raise_at_run_time(rhs) {
730 compile_into(lhs, ctx, steps);
731 compile_into(rhs, ctx, steps);
732 steps.push(Step::Binary(*op));
733 return;
734 }
735 compile_into(lhs, ctx, steps);
736 let mut rhs_steps = Vec::new();
737 compile_into(rhs, ctx, &mut rhs_steps);
738 steps.push(Step::Connective {
739 op: *op,
740 rhs: rhs_steps,
741 });
742 return;
743 }
744 let cmp = matches!(
745 op,
746 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
747 );
748 // v7.39 (enum order knife) — an enum-witnessed comparison must
749 // order by catalog member order; the VM's value-level compare
750 // cannot. Fall back to the tree evaluator for this subtree
751 // (compile-time check, zero cost when the catalog has no
752 // enum types).
753 if cmp
754 && ctx.catalog.is_some_and(|cat| !cat.enum_types().is_empty())
755 && (crate::eval::expr_enum_labels(lhs, ctx.columns, ctx.catalog).is_some()
756 || crate::eval::expr_enum_labels(rhs, ctx.columns, ctx.catalog).is_some())
757 {
758 steps.push(Step::Subtree(e.clone()));
759 return;
760 }
761 // v7.39 (round 693) — and the same move for a declared
762 // collation, which is the shape F36 had left: `loc BETWEEN 'a'
763 // AND 'd'` returns a different ROW SET under en_US.utf8 than
764 // under byte order.
765 //
766 // Compile-time, like its enum neighbour, and for the better of
767 // the two reasons. `binop::compare` is the dominant cost of a
768 // scan — its own comment measures 35.6 % of self time on
769 // `g = 5` — so a per-row collation lookup there would have to
770 // earn its place against a bench. Deciding once, while the
771 // predicate compiles, costs the scan nothing at all: a column
772 // that declares nothing never leaves the VM.
773 //
774 // Only the ORDERING operators. Measured on PG18, `=`, `<>`,
775 // LIKE, IN and count(DISTINCT …) all give byte-equality's
776 // answer under a deterministic collation.
777 if matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq)
778 && operand_declares_a_collation(lhs, ctx) | operand_declares_a_collation(rhs, ctx)
779 {
780 steps.push(Step::Subtree(e.clone()));
781 return;
782 }
783 // v7.39 (round 704) — an UNKNOWN string literal against a
784 // numeric-family operand that will NOT parse as its type. PG's
785 // error for `WHERE i = 'abc'` is the input function's
786 // (`invalid input syntax for type integer: "abc"`); the VM's
787 // value-level compare can only say "operator does not exist",
788 // so this shape leaves for the tree evaluator, whose Binary
789 // arm has the Exprs and rewrites the error. Compile-time and
790 // failure-only: a literal that parses stays on the VM path
791 // and costs nothing.
792 if cmp && unparseable_numeric_literal_cmp(lhs, rhs, ctx) {
793 steps.push(Step::Subtree(e.clone()));
794 return;
795 }
796 compile_into(lhs, ctx, steps);
797 compile_into(rhs, ctx, steps);
798 let ci = cmp
799 && (matches!(
800 column_collation(lhs, ctx),
801 Some(spg_storage::Collation::CaseInsensitive)
802 ) || matches!(
803 column_collation(rhs, ctx),
804 Some(spg_storage::Collation::CaseInsensitive)
805 ));
806 // v7.39 (round 364, M4 P2) — a MySQL session folds every text
807 // comparison, so it needs the CI step too (the step chooses
808 // the accent-aware fold at run time).
809 let ci = ci || (cmp && super::resolve::mysql_text_fold_applies(lhs, rhs, ctx));
810 steps.push(if ci {
811 Step::BinaryCi(*op)
812 } else {
813 Step::Binary(*op)
814 });
815 }
816 Expr::Unary { op, expr } => {
817 // v7.39 (round 383) — MySQL `~x` is the UNSIGNED 64-bit
818 // complement; route to the interpreter (eval.rs `mysql_bit_not`)
819 // since Step::Unary calls the dialect-blind apply_unary.
820 if ctx.mysql_dialect && matches!(op, UnOp::BitNot) {
821 steps.push(Step::Subtree(e.clone()));
822 return;
823 }
824 compile_into(expr, ctx, steps);
825 steps.push(Step::Unary(*op));
826 }
827 Expr::IsNull { expr, negated } => {
828 compile_into(expr, ctx, steps);
829 steps.push(Step::IsNull { negated: *negated });
830 }
831 Expr::InList {
832 expr,
833 list,
834 negated,
835 } => {
836 // v7.39 (round 364, M4 P2) — a MySQL session folds text before
837 // the membership test; the set-based compiled path compares
838 // raw. Route it to the interpreter, which folds (eval.rs
839 // `eval_in_list_arm`). The perf-critical InSet path is PG-only.
840 if ctx.mysql_dialect {
841 steps.push(Step::Subtree(e.clone()));
842 return;
843 }
844 // I2: the set is built at compile time. The gate
845 // (`fully_compilable`) guarantees we only reach here
846 // when the list builds a set and the needle compiles —
847 // but keep the Subtree fallback for defence in depth.
848 match crate::build_in_list_set(list) {
849 Some(entry) if fully_compilable(expr) => {
850 compile_into(expr, ctx, steps);
851 steps.push(Step::InSet {
852 set: entry.set,
853 has_null: entry.has_null,
854 negated: *negated,
855 fallback: e.clone(),
856 });
857 }
858 _ => steps.push(Step::Subtree(e.clone())),
859 }
860 }
861 Expr::Like {
862 expr,
863 pattern,
864 negated,
865 case_insensitive,
866 } => {
867 // v7.39 (round 364, M4 P2) — LIKE folds accents + case on a
868 // MySQL session (eval.rs `eval_like_arm`); the compiled
869 // pattern walk does not. Route to the interpreter.
870 if ctx.mysql_dialect {
871 steps.push(Step::Subtree(e.clone()));
872 return;
873 }
874 match literal_text_pattern(pattern) {
875 Some(pat) if fully_compilable(expr) => {
876 // v7.36 (perf — mailrs Phase 1, get_contacts hot
877 // inner) — trivial all-`%` pattern (`%`, `%%`, …)
878 // matches every non-NULL text. Collapse the LIKE
879 // into a `lhs IS NOT NULL` check: emit the operand
880 // then `IsNull { negated: !*negated }`. For ILIKE
881 // `%%` on 25 k rows the per-row `like_match_inner`
882 // → 2-char walk (~30 ns each) becomes a tag check
883 // (~3 ns); the operand still gets evaluated for the
884 // NULL semantics that SQL `LIKE` requires.
885 if !pat.is_empty() && pat.chars().all(|c| c == '%') {
886 compile_into(expr, ctx, steps);
887 steps.push(Step::AnyTextMatch { negated: *negated });
888 return;
889 }
890 compile_into(expr, ctx, steps);
891 let chars: alloc::vec::Vec<char> = if *case_insensitive {
892 pat.to_lowercase().chars().collect()
893 } else {
894 pat.chars().collect()
895 };
896 // v7.39 — `%[k×_]lit[m×_]%` runs on the substring fast
897 // path (see Step::LikeSubstring).
898 if let Some((k, needle, m)) = like_substring_shape(&chars) {
899 steps.push(Step::LikeSubstring {
900 needle,
901 k_before: k,
902 m_after: m,
903 negated: *negated,
904 case_insensitive: *case_insensitive,
905 });
906 return;
907 }
908 steps.push(Step::Like {
909 pattern: chars,
910 negated: *negated,
911 case_insensitive: *case_insensitive,
912 });
913 }
914 _ => steps.push(Step::Subtree(e.clone())),
915 }
916 }
917 // v7.39 (round 594) — a literal-pattern regex compiles here instead
918 // of once per row. `s ~ 'p'` and `s ~* 'p'` both lower to
919 // `regexp_like`, so this one shape covers the operators too. A
920 // pattern that is not a literal (or flags that are not) stays on the
921 // interpreter, which still has to compile per row: the pattern can
922 // differ row to row.
923 Expr::FunctionCall { name, args }
924 if name.eq_ignore_ascii_case("regexp_like")
925 && matches!(args.len(), 2 | 3)
926 && regex_literal_parts(args.as_slice()).is_some()
927 && fully_compilable(&args[0]) =>
928 {
929 let (pat, ci) = regex_literal_parts(args.as_slice()).expect("checked above");
930 match crate::eval::compile_re(pat, ci) {
931 Ok(re) => {
932 compile_into(&args[0], ctx, steps);
933 steps.push(Step::Regex {
934 re,
935 fallback: e.clone(),
936 });
937 }
938 // An invalid pattern is an error the interpreter words; let
939 // it keep raising it, in its own wording.
940 Err(_) => steps.push(Step::Subtree(e.clone())),
941 }
942 }
943 // v7.36 — PURE scalar function call: emit args then a
944 // single Function step that pops them. `fully_compilable`
945 // gates the whitelist + recurses into args, so this branch
946 // only fires when the entire subtree is compilable.
947 Expr::FunctionCall { name, args } if is_pure_scalar_function(name) => {
948 // v7.36 — specialise `LENGTH(<column>)` /
949 // `OCTET_LENGTH(<column>)` so the column's `Value::Text`
950 // isn't cloned just to read its length. The general
951 // `Step::Function` path goes through `apply_function`,
952 // which can't borrow off the stack — it copies.
953 let lower = name.to_ascii_lowercase();
954 if args.len() == 1 {
955 if let Expr::Column(c) = &args[0]
956 && let Some(pos) = compile_column_pos(c, ctx)
957 {
958 match lower.as_str() {
959 "length" | "char_length" | "character_length" => {
960 steps.push(Step::ColumnLength { pos });
961 return;
962 }
963 "octet_length" => {
964 steps.push(Step::ColumnOctetLength { pos });
965 return;
966 }
967 _ => {}
968 }
969 }
970 }
971 for a in args {
972 compile_into(a, ctx, steps);
973 }
974 // v7.39 (round 621) — COALESCE / NULLIF compile to their own
975 // steps (see the variants) so the chosen argument stays borrowed.
976 // The arguments are already on the stack from the loop above — a
977 // first cut recompiled them here and doubled them.
978 if name.eq_ignore_ascii_case("coalesce") && !args.is_empty() {
979 steps.push(Step::Coalesce { n_args: args.len() });
980 return;
981 }
982 if name.eq_ignore_ascii_case("nullif") && args.len() == 2 {
983 steps.push(Step::NullIf);
984 return;
985 }
986 // v7.39 (round 717) — GREATEST / LEAST get their own step;
987 // see the variant.
988 if (lower == "greatest" || lower == "least") && !args.is_empty() {
989 steps.push(Step::Extremum {
990 n_args: args.len(),
991 max: lower == "greatest",
992 });
993 return;
994 }
995 steps.push(Step::Function {
996 name_lower: lower,
997 n_args: args.len(),
998 });
999 }
1000 // v7.39 (round 605) — a CONSTANT subexpression is evaluated once here
1001 // rather than for every row. `WHERE id < ('500')::INT` cost two
1002 // allocations a row against none for `WHERE id < 500`, and the same
1003 // gap is much wider in a projection. A literal is already a `Lit`
1004 // step, so this is only about the shapes built OUT of literals.
1005 //
1006 // An error stays where it was: if the fold does not evaluate, the
1007 // expression compiles as before and raises per row, in the
1008 // interpreter's own wording.
1009 e if !matches!(e, Expr::Literal(_)) && constant_expr(e) => {
1010 match eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx) {
1011 Ok(v) => steps.push(Step::Lit(v)),
1012 Err(_) => steps.push(Step::Subtree(e.clone())),
1013 }
1014 }
1015 // v7.39 (round 597) — `x = ANY (ARRAY[literals])` is `x IN (…)` and
1016 // `x <> ALL (…)` is `x NOT IN (…)`, down to the three-valued
1017 // treatment of a NULL element, so they take the membership set the
1018 // IN list already builds at compile time: 40.9 ms for a ten-element
1019 // array against 2.1 for the IN spelling of the same question. Folding
1020 // the array (below) alone left the per-row cost growing with the
1021 // array's length; a set does not.
1022 Expr::AnyAll {
1023 expr,
1024 op,
1025 array,
1026 is_any,
1027 } if !ctx.mysql_dialect
1028 && ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
1029 || (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
1030 && array_literal_items(array)
1031 .is_some_and(|it| !it.is_empty() && crate::build_in_list_set(it).is_some())
1032 && fully_compilable(expr) =>
1033 {
1034 let items = array_literal_items(array).expect("checked above");
1035 let entry = crate::build_in_list_set(items).expect("checked above");
1036 compile_into(expr, ctx, steps);
1037 steps.push(Step::InSet {
1038 set: entry.set,
1039 has_null: entry.has_null,
1040 negated: !*is_any,
1041 fallback: e.clone(),
1042 });
1043 }
1044 // v7.39 (round 597) — any other ANY/ALL whose right-hand array is
1045 // constant: build it once here rather than per row.
1046 Expr::AnyAll {
1047 expr,
1048 op,
1049 array,
1050 is_any,
1051 } if constant_expr(array) => {
1052 match eval_expr(array, &Row::new(alloc::vec::Vec::new()), ctx) {
1053 Ok(arr) => {
1054 compile_into(expr, ctx, steps);
1055 // v7.39 (round 604) — with the array in hand, an equality
1056 // ANY / inequality ALL is a membership test whatever the
1057 // spelling: `'{1,2,3}'::int[]` keeps its elements inside a
1058 // string, so round 597's literal-list route could not see
1059 // them, but they are values now.
1060 if !ctx.mysql_dialect
1061 && ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
1062 || (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
1063 && let Some(entry) = value_array_in_list_set(&arr)
1064 {
1065 steps.push(Step::InSet {
1066 set: entry.set,
1067 has_null: entry.has_null,
1068 negated: !*is_any,
1069 fallback: e.clone(),
1070 });
1071 return;
1072 }
1073 steps.push(Step::AnyAll {
1074 op: *op,
1075 is_any: *is_any,
1076 arr,
1077 });
1078 }
1079 // A constant that does not evaluate is the interpreter's
1080 // error to raise, per row, in its own wording.
1081 Err(_) => steps.push(Step::Subtree(e.clone())),
1082 }
1083 }
1084 // v7.39 (round 595) — EXTRACT over a compilable source. One
1085 // non-compilable node used to disqualify the WHOLE predicate, so
1086 // `WHERE extract(year FROM t) = 2020` interpreted the column read
1087 // and the comparison as well: 81.7 ms on 500k rows against PG18's
1088 // 14.5, where a compiled comparison on the same column is 13.1.
1089 Expr::Extract { field, source } => {
1090 compile_into(source, ctx, steps);
1091 steps.push(Step::Extract {
1092 field: field.clone(),
1093 fallback: e.clone(),
1094 });
1095 }
1096 Expr::Cast { expr, target } => {
1097 // v7.39 (read01 ruleutils.c) — catalog-dependent casts run
1098 // through eval's pre-hook (regclass dual-shape, domain/enum/
1099 // composite named types).
1100 // v7.39 (round 621) — the varchar/char FAMILY is catalog-free, so
1101 // it stays on the compiled path; the blanket Named -> Subtree rule
1102 // sent `s::VARCHAR(20)` to the interpreter, which pays two
1103 // allocations a row. Everything else Named (domains, enums,
1104 // composites, regtypes) still needs the interpreter's catalog.
1105 let named_text_family = match target {
1106 spg_sql::ast::CastTarget::Named(n) => named_varchar_family(n),
1107 _ => false,
1108 };
1109 // v7.39 (round 722) — a plain scalar spelling resolves NOW, not
1110 // per row; see `Step::CastPlain`. The text family keeps its
1111 // dedicated route (the timestamptz::text Subtree guard below
1112 // must still see it).
1113 if let spg_sql::ast::CastTarget::Named(n) = target
1114 && !named_text_family
1115 && let Some(dt) = super::cast::plain_named_target(n)
1116 {
1117 compile_into(expr, ctx, steps);
1118 steps.push(Step::CastPlain {
1119 dt,
1120 name: n.clone(),
1121 });
1122 return;
1123 }
1124 if matches!(target, spg_sql::ast::CastTarget::RegClass)
1125 || (matches!(target, spg_sql::ast::CastTarget::Named(_)) && !named_text_family)
1126 {
1127 steps.push(Step::Subtree(e.clone()));
1128 return;
1129 }
1130 // v7.39 (read01 round 76) — `<timestamptz>::text` renders the
1131 // `+00` offset, and tz-ness lives in the *static* type, not in
1132 // the runtime `Value::Timestamp`. `Step::Cast` calls the pure
1133 // `cast_value(value, target)`, which cannot see the expression
1134 // it came from — so a cast the interpreter renders with an
1135 // offset came out without one whenever the compiled VM drove
1136 // it (every cast inside an aggregate argument, and every cast
1137 // over an aggregate result: `string_agg(x::text, ',')`,
1138 // `min(x)::text`). Keep this one shape on Subtree.
1139 if (matches!(target, spg_sql::ast::CastTarget::Text) || named_text_family)
1140 && crate::describe::describe_expr(expr, ctx.columns)
1141 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
1142 {
1143 steps.push(Step::Subtree(e.clone()));
1144 return;
1145 }
1146 compile_into(expr, ctx, steps);
1147 steps.push(Step::Cast {
1148 target: target.clone(),
1149 });
1150 }
1151 Expr::Case {
1152 operand,
1153 branches,
1154 else_branch,
1155 } => {
1156 // Gate by `fully_compilable` at the leaf: if any sub-expr
1157 // can't compile natively, the whole Case stays Subtree so
1158 // a single Case never escapes to a row-materialise eval.
1159 let all_ok = operand.as_deref().is_none_or(fully_compilable)
1160 && branches
1161 .iter()
1162 .all(|(w, t)| fully_compilable(w) && fully_compilable(t))
1163 && else_branch.as_deref().is_none_or(fully_compilable);
1164 if !all_ok {
1165 steps.push(Step::Subtree(e.clone()));
1166 return;
1167 }
1168 let op_c = operand.as_deref().map(|o| compile_expr(o, ctx));
1169 let branches_c: alloc::vec::Vec<(CompiledExpr, CompiledExpr)> = branches
1170 .iter()
1171 .map(|(w, t)| (compile_expr(w, ctx), compile_expr(t, ctx)))
1172 .collect();
1173 let else_c = else_branch.as_deref().map(|el| compile_expr(el, ctx));
1174 // Either side byte-wise keeps the whole operand match byte-wise,
1175 // the rule `mysql_text_fold_applies` states for a comparison's
1176 // two operands — a CASE arm is that comparison.
1177 let fold_operand = operand.as_deref().is_some_and(|o| {
1178 branches
1179 .iter()
1180 .all(|(w, _)| super::resolve::mysql_text_fold_applies(o, w, ctx))
1181 });
1182 steps.push(Step::Case {
1183 fold_operand,
1184 operand: op_c,
1185 branches: branches_c,
1186 else_branch: else_c,
1187 });
1188 // v7.38 (read01) — resolve the CASE result to PG's common type of
1189 // every THEN/ELSE branch once, here, and append a scale-preserving
1190 // coercion so a taken integer branch is widened to numeric (and
1191 // `pg_typeof` / downstream division match PG). Costs nothing when
1192 // the branches already share a type (common_type → None).
1193 let branch_types: Vec<spg_storage::DataType> = branches
1194 .iter()
1195 .map(|(_, t)| t)
1196 .chain(else_branch.iter().map(|b| b.as_ref()))
1197 .filter_map(|e| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
1198 .collect();
1199 if let Some(common) = crate::describe::common_type(&branch_types) {
1200 steps.push(Step::CoerceCommon(common));
1201 }
1202 }
1203 other => steps.push(Step::Subtree(other.clone())),
1204 }
1205}
1206
1207/// Literal text pattern behind a LIKE/ILIKE, if any.
1208/// v7.39 — recognise `%[k×_]literal[m×_]%` (any number of leading /
1209/// trailing `%`; literal free of `%` / `_` / `\`). Returns
1210/// `(k, literal, m)` when the pattern fits the substring fast path.
1211fn like_substring_shape(pat: &[char]) -> Option<(usize, alloc::string::String, usize)> {
1212 let mut lo = 0;
1213 while lo < pat.len() && pat[lo] == '%' {
1214 lo += 1;
1215 }
1216 if lo == 0 {
1217 return None; // not %-anchored at the front
1218 }
1219 let mut hi = pat.len();
1220 while hi > lo && pat[hi - 1] == '%' {
1221 hi -= 1;
1222 }
1223 if hi == pat.len() {
1224 return None; // not %-anchored at the back
1225 }
1226 let inner = &pat[lo..hi];
1227 let mut i = 0;
1228 while i < inner.len() && inner[i] == '_' {
1229 i += 1;
1230 }
1231 let mut j = inner.len();
1232 while j > i && inner[j - 1] == '_' {
1233 j -= 1;
1234 }
1235 let lit = &inner[i..j];
1236 if lit.is_empty() || lit.iter().any(|&c| c == '%' || c == '_' || c == '\\') {
1237 return None;
1238 }
1239 Some((i, lit.iter().collect(), inner.len() - j))
1240}
1241
1242/// v7.39 — `%[k×_]needle[m×_]%` matcher: walk `str::find` hits of the
1243/// literal and accept one with ≥k chars before it and ≥m chars after.
1244/// v7.39 (round 484) — find `needle` in `hay` at or after `start`.
1245///
1246/// `str::find(&str)` runs the two-way algorithm, and its SETUP is the cost:
1247/// round 484's profile of `s LIKE '%_05%'` put `StrSearcher::new` at 14.6 %
1248/// of self time — rebuilt for every row against a needle that is a compile
1249/// -time constant, and only two bytes long here.
1250///
1251/// An ASCII needle can be scanned as bytes instead: a UTF-8 continuation
1252/// byte is always >= 0x80, so an ASCII byte match can never land inside a
1253/// multi-byte character and every hit is on a char boundary. A non-ASCII
1254/// needle keeps `find`, where that reasoning does not hold.
1255fn like_find_from(hay: &str, needle: &str, start: usize) -> Option<usize> {
1256 if needle.is_empty() {
1257 return Some(start);
1258 }
1259 if !needle.is_ascii() {
1260 return hay[start..].find(needle).map(|rel| start + rel);
1261 }
1262 let h = hay.as_bytes();
1263 let n = needle.as_bytes();
1264 if h.len() < n.len() {
1265 return None;
1266 }
1267 let last = h.len() - n.len();
1268 let mut i = start;
1269 while i <= last {
1270 let off = h[i..=last].iter().position(|&b| b == n[0])?;
1271 let at = i + off;
1272 if &h[at..at + n.len()] == n {
1273 return Some(at);
1274 }
1275 i = at + 1;
1276 }
1277 None
1278}
1279
1280fn like_substring_match(hay: &str, needle: &str, k: usize, m: usize) -> bool {
1281 let mut start = 0;
1282 while let Some(off) = like_find_from(hay, needle, start) {
1283 let before_ok = k == 0 || hay[..off].chars().take(k).count() == k;
1284 let after_ok = m == 0 || hay[off + needle.len()..].chars().take(m).count() == m;
1285 if before_ok && after_ok {
1286 return true;
1287 }
1288 // Advance one char past this hit's start and retry.
1289 match hay[off..].chars().next() {
1290 Some(c) => start = off + c.len_utf8(),
1291 None => return false,
1292 }
1293 }
1294 false
1295}
1296
1297fn literal_text_pattern(pattern: &Expr) -> Option<&str> {
1298 match pattern {
1299 Expr::Literal(Literal::String(s)) => Some(s.as_str()),
1300 _ => None,
1301 }
1302}
1303
1304/// True when the whole tree consists of nodes the compiler models
1305/// natively. Mixed trees stay on the interpreted path: a Subtree
1306/// fallback would run WITHOUT the per-query MemoizeCache, and
1307/// memo-dependent nodes (InList set fast path — round-25) rebuild
1308/// per row there. Measured: compiling a search WHERE with an
1309/// InList subtree regressed 634 ms → 18.7 s.
1310/// v7.39 (round 621) — is this cast an identity on THIS value?
1311///
1312/// `s::TEXT` over a text cell changes nothing, and neither does an unbounded
1313/// `::VARCHAR`; the compiled path used to clone the cell anyway. Only the
1314/// pairs that provably change nothing are listed — a bounded VARCHAR(n) must
1315/// still check its length, numerics their range — so an unlisted pair merely
1316/// keeps the owned path, never a wrong answer.
1317/// The catalog-free varchar/char family, in the canonical `name(p)` spelling
1318/// the parser produces. Only these Named targets stay on the compiled path.
1319fn named_varchar_family(n: &str) -> bool {
1320 let base = n.split('(').next().unwrap_or(n);
1321 base.eq_ignore_ascii_case("varchar")
1322 || base.eq_ignore_ascii_case("text")
1323 || base.eq_ignore_ascii_case("char")
1324 || base.eq_ignore_ascii_case("bpchar")
1325 || base.eq_ignore_ascii_case("character")
1326}
1327
1328/// `varchar(k)`'s k, when the name carries one.
1329fn varchar_limit(n: &str) -> Option<usize> {
1330 let base = n.split('(').next().unwrap_or(n);
1331 if !base.eq_ignore_ascii_case("varchar") {
1332 return None;
1333 }
1334 let inner = n.split('(').nth(1)?.strip_suffix(')')?;
1335 inner.trim().parse().ok()
1336}
1337
1338fn cast_is_identity_for(v: &Value<'_>, target: &spg_sql::ast::CastTarget) -> bool {
1339 match (v, target) {
1340 (Value::Text(_), spg_sql::ast::CastTarget::Text) => true,
1341 (Value::Text(t), spg_sql::ast::CastTarget::Named(n)) => {
1342 // Unbounded text and varchar change nothing. A BOUNDED varchar is
1343 // an identity exactly when the text is within its limit — VARCHAR
1344 // truncates and never pads. CHAR(n) pads, so it is never one.
1345 n.eq_ignore_ascii_case("text")
1346 || n.eq_ignore_ascii_case("varchar")
1347 || varchar_limit(n).is_some_and(|k| t.chars().take(k + 1).count() <= k)
1348 }
1349 (Value::Int(_), spg_sql::ast::CastTarget::Int) => true,
1350 (Value::BigInt(_), spg_sql::ast::CastTarget::BigInt) => true,
1351 (Value::Float(_), spg_sql::ast::CastTarget::Float) => true,
1352 (Value::Bool(_), spg_sql::ast::CastTarget::Bool) => true,
1353 _ => false,
1354 }
1355}
1356
1357pub(crate) fn fully_compilable(e: &Expr) -> bool {
1358 match e {
1359 Expr::Literal(_) | Expr::Column(_) => true,
1360 Expr::Binary { lhs, rhs, .. } => fully_compilable(lhs) && fully_compilable(rhs),
1361 Expr::Unary { expr, .. } | Expr::IsNull { expr, .. } => fully_compilable(expr),
1362 // I2: an InList is compilable ONLY when it becomes a real
1363 // InSet (all-literal list + compilable needle). A
1364 // non-set-able InList must keep the whole tree off the
1365 // compiled path so it never degrades to a memo-less,
1366 // O(list) per-row Subtree (the round-25 18.7 s trap).
1367 Expr::InList { expr, list, .. } => {
1368 fully_compilable(expr) && crate::build_in_list_set(list).is_some()
1369 }
1370 Expr::Like { expr, pattern, .. } => {
1371 fully_compilable(expr) && literal_text_pattern(pattern).is_some()
1372 }
1373 // v7.36 (perf — mailrs Ask 1) — PURE scalar functions over
1374 // compilable args go to `Step::Function`. The whitelist
1375 // covers the high-traffic / non-volatile cases; anything
1376 // outside (NOW, RANDOM, sequence accessors, EXTRACT-with-
1377 // context-dependent fields, etc.) stays on Subtree where
1378 // the interpreter has the full ctx.
1379 // v7.39 (round 594) — a `regexp_like` with a LITERAL pattern is
1380 // compilable even though the function is not on the pure list: the
1381 // pattern becomes a compile product (`Step::Regex`) rather than an
1382 // argument the step would have to re-parse per row. A non-literal
1383 // pattern stays off, because then it really can differ row to row.
1384 Expr::FunctionCall { name, args }
1385 if name.eq_ignore_ascii_case("regexp_like")
1386 && matches!(args.len(), 2 | 3)
1387 && regex_literal_parts(args.as_slice()).is_some() =>
1388 {
1389 fully_compilable(&args[0])
1390 }
1391 Expr::FunctionCall { name, args } => {
1392 is_pure_scalar_function(name) && args.iter().all(fully_compilable)
1393 }
1394 // v7.36 — CAST over a compilable expression. `cast_value`
1395 // is pure / context-free for the scalar targets we care
1396 // about (text, ints, floats, bool, dates).
1397 // v7.39 (read01 ruleutils.c) — regclass / user-named casts
1398 // need the catalog (dual-shape resolve, domain/enum/composite
1399 // hooks); they stay Subtree so eval's pre-hook runs.
1400 Expr::AnyAll { expr, array, .. } if constant_expr(array) => fully_compilable(expr),
1401 Expr::Extract { source, .. } => fully_compilable(source),
1402 Expr::Cast { expr, target } => {
1403 // v7.39 (round 621) — the varchar/char family is catalog-free and
1404 // compiles (the compile arm gates it the same way); other Named
1405 // targets still need eval's catalog pre-hooks.
1406 let target_ok = match target {
1407 spg_sql::ast::CastTarget::RegClass => false,
1408 // v7.39 (round 722) — a compile-time-resolvable plain name
1409 // is as compilable as the dedicated variants; see
1410 // `Step::CastPlain`.
1411 spg_sql::ast::CastTarget::Named(n) => {
1412 named_varchar_family(n) || super::cast::plain_named_target(n).is_some()
1413 }
1414 _ => true,
1415 };
1416 target_ok && fully_compilable(expr)
1417 }
1418 // v7.37.5-A2b — `CASE [operand] WHEN x THEN y … ELSE z END`
1419 // when every sub-expression is itself fully-compilable. Hot
1420 // shape: Track A's 14 aggregates over
1421 // `COUNT(DISTINCT CASE WHEN m.message_id != '' THEN
1422 // m.message_id
1423 // ELSE CAST(m.id AS TEXT) END)` — without
1424 // this, every Case fell to `arg_compiled = None`, forced
1425 // `needs_mat = true` per-row, and triggered a full combined-
1426 // row `Vec<Value>` clone for the eval path.
1427 Expr::Case {
1428 operand,
1429 branches,
1430 else_branch,
1431 } => {
1432 operand.as_deref().is_none_or(fully_compilable)
1433 && branches
1434 .iter()
1435 .all(|(w, t)| fully_compilable(w) && fully_compilable(t))
1436 && else_branch.as_deref().is_none_or(fully_compilable)
1437 }
1438 _ => false,
1439 }
1440}
1441
1442/// v7.39 (round 595) — functions that are NOT context-free but ARE fixed for
1443/// the whole statement: they read the session's time zone, DateStyle or
1444/// lc_time out of the `EvalContext`, and `Step::Function` hands that context
1445/// to `apply_function_lower` exactly as the interpreter would.
1446///
1447/// Keeping them off the compiled path cost the whole predicate, not just the
1448/// call: one non-compilable node disqualifies the entire WHERE, so
1449/// `WHERE date_trunc('day', t) = TIMESTAMP '…'` interpreted the column read
1450/// and the comparison too — 153.8 ms over 500k rows against PG18's 9.7,
1451/// where a compiled comparison on the same column is 13.1.
1452///
1453/// `now` / `random` / sequence accessors stay off: they are not fixed for
1454/// the statement in the way these are.
1455fn is_session_deterministic_function(name: &str) -> bool {
1456 matches!(
1457 name.to_ascii_lowercase().as_str(),
1458 // v7.39 (round 717) — `format` belongs here, not on the pure
1459 // list: it renders arguments through the SESSION's RenderStyle
1460 // (datestyle / extra_float_digits / bytea_output), exactly the
1461 // dependency class to_char carries. Its absence from BOTH lists
1462 // was the round-716 panel's 4.89× cell — the only remaining
1463 // text-shape loss that was pure fallback tax.
1464 "date_trunc" | "date_part" | "to_char" | "age" | "format"
1465 )
1466}
1467
1468/// v7.36 — PURE scalar function whitelist for `Step::Function`.
1469/// "Pure" means: deterministic, context-independent, no side
1470/// effects. Aggregate names (sum / count / max / …) are filtered
1471/// upstream by the caller — they never reach the compiler. NOW /
1472/// RANDOM / sequence accessors are excluded because they need the
1473/// `EvalContext`'s clock / sequence resolver and aren't
1474/// deterministic. EXTRACT is excluded because the field kind is
1475/// parsed off the Expr tree, not an arg.
1476fn is_pure_scalar_function(name: &str) -> bool {
1477 is_session_deterministic_function(name)
1478 || matches!(
1479 name.to_ascii_lowercase().as_str(),
1480 // string length + slicing
1481 "length"
1482 | "char_length"
1483 | "character_length"
1484 | "octet_length"
1485 | "upper"
1486 | "lower"
1487 | "trim"
1488 | "ltrim"
1489 | "rtrim"
1490 | "btrim"
1491 | "left"
1492 | "right"
1493 | "substring"
1494 | "substr"
1495 | "replace"
1496 | "position"
1497 | "strpos"
1498 | "concat"
1499 | "concat_ws"
1500 | "reverse"
1501 | "repeat"
1502 | "lpad"
1503 | "rpad"
1504 | "split_part"
1505 // v7.39 (round 728) — the JSON constructors: pure over
1506 // their arguments (JSON's number/text rendering is fixed
1507 // by the format, not the session's RenderStyle — probed
1508 // against the ::JSONB cast lane, already whitelisted).
1509 // v7.39 (round 730) — the digest family: pure bytes-in,
1510 // hex/bytea-out. count(md5(s)) was the panel's last
1511 // serial-lane text cell (2.37×): the hash itself is
1512 // ~40% faster than PG's per call here, and ALL of the
1513 // loss was the missing parallel lane.
1514 | "md5"
1515 | "sha224"
1516 | "sha256"
1517 | "sha384"
1518 | "sha512"
1519 | "to_json"
1520 | "to_jsonb"
1521 | "jsonb_build_object"
1522 | "json_build_object"
1523 | "jsonb_build_array"
1524 | "json_build_array"
1525 // null/conditional
1526 | "coalesce"
1527 | "nullif"
1528 | "greatest"
1529 | "least"
1530 | "ifnull"
1531 | "isnull"
1532 | "nvl"
1533 // numeric
1534 | "abs"
1535 | "ceil"
1536 | "ceiling"
1537 | "floor"
1538 | "round"
1539 | "trunc"
1540 | "sqrt"
1541 | "power"
1542 | "pow"
1543 | "mod"
1544 | "sign"
1545 | "log"
1546 | "log10"
1547 | "exp"
1548 | "ln"
1549 // boolean / cast helpers
1550 | "cast"
1551 )
1552}
1553
1554/// r1021 — the arithmetic the integer lane runs. `Div` and `Mod` are in
1555/// because their zero divisor is handled by falling back, not by guessing.
1556const fn is_int_arithmetic(op: BinOp) -> bool {
1557 matches!(
1558 op,
1559 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
1560 )
1561}
1562
1563/// r1021 — the comparison that ends an integer-lane program.
1564const fn is_int_comparison(op: BinOp) -> bool {
1565 matches!(
1566 op,
1567 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
1568 )
1569}
1570
1571/// r1021 — `(value, is_32_bit)` for the two widths the lane admits. NULL,
1572/// `smallint` and every non-integer answer `None`, which sends the row to
1573/// the ordinary machine.
1574const fn int_operand(v: &Value<'_>) -> Option<(i64, bool)> {
1575 match v {
1576 Value::Int(i) => Some((*i as i64, true)),
1577 Value::BigInt(i) => Some((*i, false)),
1578 _ => None,
1579 }
1580}
1581
1582pub(crate) fn compile_expr(e: &Expr, ctx: &EvalContext<'_>) -> CompiledExpr {
1583 let mut steps = Vec::new();
1584 compile_into(e, ctx, &mut steps);
1585 let mut c = CompiledExpr {
1586 steps,
1587 pred_shape: PredShape::Other,
1588 };
1589 // Classified through the very matchers the row loop will use, so the
1590 // label and the destructuring cannot disagree.
1591 c.pred_shape = if c.as_column_cmp_literal().is_some() {
1592 PredShape::ColumnCmpLit
1593 } else if c.as_column_in_set().is_some() {
1594 PredShape::ColumnInSet
1595 } else if c.as_column_like().is_some() {
1596 PredShape::ColumnLike
1597 } else if c.is_int_arith_pred() {
1598 PredShape::IntArith
1599 } else {
1600 PredShape::Other
1601 };
1602 c
1603}
1604
1605/// Run a compiled program. `stack` is caller-owned scratch
1606/// (cleared here) so tight row loops never touch the allocator
1607/// for the machine itself.
1608pub(crate) fn eval_compiled(
1609 c: &CompiledExpr,
1610 row: &Row<'static>,
1611 ctx: &EvalContext<'_>,
1612 stack: &mut Vec<Value<'static>>,
1613) -> Result<Value<'static>, EvalError> {
1614 // v7.37.16 — reuse the caller's stack allocation across rows.
1615 // v7.37.9 T3 S2 had severed this: `eval_compiled_ref` pushes
1616 // `Value<'val>` where `'val` is the per-call RowRef borrow, and
1617 // `Vec<Value<'val>>` is invariant in `'val`, so the caller's
1618 // `Vec<Value<'static>>` could not be lent in-place and every call
1619 // allocated a fresh local Vec. That was sized for the ~50×/query
1620 // post-group projection path, but the aggregate/scan WHERE filter
1621 // loops (select.rs) call this once PER ROW — 50 k allocs/query on
1622 // a 50 k-row filter (the heavy.rs filter_agg 1.5×-vs-PG18 loss).
1623 // Instead: MOVE the caller's Vec in (covariant shrink 'static →
1624 // 'val, safe), run, then hand the emptied allocation back via
1625 // `recycle_stack`. Zero per-row alloc; the borrowed-push (S2/S3)
1626 // zero-clone Text path is untouched.
1627 let rowref = crate::join::RowRef::Owned(row);
1628 let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
1629 let result = eval_compiled_ref(c, rowref, ctx, &mut local_stack);
1630 let owned = result.map(Value::into_owned);
1631 *stack = recycle_stack(local_stack);
1632 owned
1633}
1634
1635/// v7.39 (round 479) — evaluate a compiled WHERE and answer the bool,
1636/// without ever materialising an owned `Value`.
1637///
1638/// `eval_compiled` ends in `result.map(Value::into_owned)` because its
1639/// contract is to hand back a `Value<'static>`. A predicate does not want
1640/// a value at all — it wants one bool — and round 478's profile put
1641/// `Value::into_owned` at 5.8 % of self time and `drop_glue<Value>` at
1642/// 15.1 %, against 5.5 % for the comparison the predicate exists to
1643/// perform. The `into_owned` and the owned value's drop are both pure
1644/// overhead on this path.
1645///
1646/// Everything else is `eval_compiled`'s bridge unchanged: the caller's
1647/// stack is moved in (covariant shrink), run, and handed back emptied.
1648pub(crate) fn eval_compiled_pred(
1649 c: &CompiledExpr,
1650 row: &Row<'static>,
1651 ctx: &EvalContext<'_>,
1652 stack: &mut Vec<Value<'static>>,
1653 mysql: bool,
1654) -> Result<bool, EvalError> {
1655 // The shape was settled at compile time; the row loop reads one
1656 // discriminant instead of re-matching the step list per row.
1657 match c.pred_shape {
1658 // r1021 — integer arithmetic runs on an i64 stack, building no
1659 // `Value` at all. `None` means the row carried something the lane
1660 // does not decide (NULL, a non-integer, a zero divisor, an
1661 // overflow) and the ordinary machine below answers it instead.
1662 PredShape::IntArith => {
1663 if let Some(verdict) = c.eval_int_arith_pred(row) {
1664 crate::bump_counter!(STEP_VM_INTLANE_FIRE);
1665 return Ok(verdict);
1666 }
1667 crate::bump_counter!(STEP_VM_INTLANE_FALLBACK);
1668 }
1669 // v7.39 (round 482) — `<column> <cmp> <literal>` compares in place.
1670 //
1671 // The general path builds three `Value`s a row and drops them;
1672 // this one reads both operands by reference and builds only the
1673 // comparison result. `apply_binary_by_ref` is the SAME function
1674 // `Step::Binary` reaches for first, so the answer is identical by
1675 // construction rather than by a second reading of the semantics.
1676 PredShape::ColumnCmpLit => {
1677 if let Some((pos, op, lit)) = c.as_column_cmp_literal() {
1678 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1679 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1680 if let Some(res) = super::apply_binary_by_ref(op, cell, lit)? {
1681 return crate::eval::predicate_is_true(&res, "WHERE", mysql);
1682 }
1683 // The by-ref form declined (an op that builds an owned
1684 // result); fall through rather than answer differently
1685 // from the VM.
1686 }
1687 }
1688 // v7.39 (round 486) — `<column> [NOT] IN (<literals>)` looks the
1689 // cell up in place. Same `in_set_verdict` the `InSet` step calls,
1690 // so the answer is identical by construction; a family mismatch
1691 // returns None and falls through to the general path, which takes
1692 // the step's interpreter fallback.
1693 PredShape::ColumnInSet => {
1694 if let Some((pos, set, has_null, negated)) = c.as_column_in_set() {
1695 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1696 if let Some(v) = in_set_verdict(cell, set, has_null, negated) {
1697 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1698 return crate::eval::predicate_is_true(&v, "WHERE", mysql);
1699 }
1700 }
1701 }
1702 // v7.39 (round 488) — `<column> [NOT] [I]LIKE '<literal>'` matches
1703 // straight off the cell. The matcher wanted a `&str` all along;
1704 // the VM was pushing a `Value` and popping it for no other reason.
1705 PredShape::ColumnLike => {
1706 if let Some((pos, step)) = c.as_column_like() {
1707 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1708 if let Some(v) = like_verdict(cell, step) {
1709 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1710 return crate::eval::predicate_is_true(&v?, "WHERE", mysql);
1711 }
1712 }
1713 }
1714 PredShape::Other => {}
1715 }
1716 let rowref = crate::join::RowRef::Owned(row);
1717 let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
1718 let verdict = eval_compiled_ref(c, rowref, ctx, &mut local_stack)
1719 .and_then(|v| crate::eval::predicate_is_true(&v, "WHERE", mysql));
1720 *stack = recycle_stack(local_stack);
1721 verdict
1722}
1723
1724/// v7.39 (round 486) — the membership decision, shared by `Step::InSet`
1725/// and by the fast predicate below so the two cannot drift. `None` means
1726/// the needle's family does not match the set's, which is the caller's
1727/// cue to take the interpreter's coercion path on the whole node.
1728///
1729/// v7.39 (round 489) — `#[inline(always)]` is load-bearing, and the
1730/// measurement behind it is worth stating because round 486 got it wrong.
1731/// Round 486 saw the shared-helper form cost `like_filter` 4.5 % and
1732/// concluded "editing this loop is expensive"; it then duplicated the
1733/// body into the arm to avoid touching it. Re-measured with the shape
1734/// ISOLATED (round 488 found the panel's shapes contaminate each other),
1735/// `like_filter` shows no such cost — that reading was its neighbours.
1736/// What IS real is `big_in`: +4.6 % with a plain call, separated spreads,
1737/// on a shape that takes the fast path and never executes this arm.
1738/// `#[inline(always)]` returns it to parity (-0.1 %, overlapping), so the
1739/// duplicate bought nothing and is gone.
1740///
1741/// `e2e_in_set_fast_path_round486` still runs every needle × set ×
1742/// negated × has-NULL combination down BOTH entry points.
1743#[allow(clippy::inline_always)] // measured: see the note above
1744#[inline(always)]
1745fn in_set_verdict(
1746 needle: &Value<'_>,
1747 set: &crate::memoize::InListSet,
1748 has_null: bool,
1749 negated: bool,
1750) -> Option<Value<'static>> {
1751 let contained = match (needle, set) {
1752 // Non-empty list + NULL needle → NULL (NOT NULL is still NULL) —
1753 // matches the interpreter and eval_with_in_sets.
1754 (Value::Null, _) => return Some(Value::Null),
1755 (Value::SmallInt(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
1756 (Value::Int(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
1757 (Value::BigInt(n), crate::memoize::InListSet::Int(s)) => s.contains(n),
1758 (Value::Text(t), crate::memoize::InListSet::Text(s)) => s.contains(t.as_ref()),
1759 _ => return None,
1760 };
1761 let inner = if contained {
1762 Value::Bool(true)
1763 } else if has_null {
1764 Value::Null
1765 } else {
1766 Value::Bool(false)
1767 };
1768 Some(match (negated, inner) {
1769 (true, Value::Bool(b)) => Value::Bool(!b),
1770 (_, v) => v,
1771 })
1772}
1773
1774/// v7.39 (round 604) — the membership set of an ALREADY-EVALUATED constant
1775/// array.
1776///
1777/// Round 597 gave `x = ANY (ARRAY[1,2,3])` the same set an IN list builds,
1778/// which took it from 268 ms over 500k rows to 1.93. It could not do the
1779/// same for `x = ANY ('{1,2,3}'::int[])`, because it built the set from AST
1780/// literals and that spelling keeps its elements inside a string: the array
1781/// was folded once but every row still walked it, and the shape stayed at
1782/// 43.49 ms against PG18's 9.37. The array has been evaluated by the time
1783/// this is asked, so the elements are right there.
1784///
1785/// The families are the ones `build_in_list_set` accepts, for the same
1786/// reason: an integer set answers `Int = BigInt` correctly across widths,
1787/// and a text set compares verbatim. Anything else — a mixed array, floats,
1788/// NUMERIC, dates — returns `None` and keeps the folded-array walk.
1789fn value_array_in_list_set(arr: &Value<'_>) -> Option<crate::memoize::InListSetEntry> {
1790 let len = crate::eval::values::array_len(arr)?;
1791 if len == 0 {
1792 return None;
1793 }
1794 let mut ints: hashbrown::HashSet<i64> = hashbrown::HashSet::with_capacity(len);
1795 let mut texts: hashbrown::HashSet<alloc::string::String> =
1796 hashbrown::HashSet::with_capacity(len);
1797 let mut has_null = false;
1798 for i in 0..len {
1799 match crate::eval::values::array_element_at(arr, i) {
1800 None | Some(Value::Null) => has_null = true,
1801 Some(Value::SmallInt(n)) => {
1802 ints.insert(i64::from(n));
1803 }
1804 Some(Value::Int(n)) => {
1805 ints.insert(i64::from(n));
1806 }
1807 Some(Value::BigInt(n)) => {
1808 ints.insert(n);
1809 }
1810 Some(Value::Text(s) | Value::BpChar(s)) => {
1811 texts.insert(s.into_owned());
1812 }
1813 _ => return None,
1814 }
1815 if !ints.is_empty() && !texts.is_empty() {
1816 return None;
1817 }
1818 }
1819 let set = if !ints.is_empty() {
1820 crate::memoize::InListSet::Int(ints)
1821 } else if !texts.is_empty() {
1822 crate::memoize::InListSet::Text(texts)
1823 } else {
1824 return None;
1825 };
1826 Some(crate::memoize::InListSetEntry { set, has_null })
1827}
1828
1829/// v7.39 (round 597) — the literal elements of an `ARRAY[…]` constructor.
1830/// `None` for any other right-hand side, including the `'{1,2}'::int[]`
1831/// spelling, whose elements live inside a string rather than the tree.
1832fn array_literal_items(e: &Expr) -> Option<&[Expr]> {
1833 match e {
1834 Expr::Array(items) if items.iter().all(constant_expr) => Some(items.as_slice()),
1835 _ => None,
1836 }
1837}
1838
1839/// v7.39 (round 605) — the value of a projection item that cannot depend on
1840/// the row, evaluated once. `None` for anything that depends on a row, or
1841/// that fails to evaluate — the latter so its error still comes from the row
1842/// loop, in the interpreter's own wording, rather than from planning.
1843pub(crate) fn constant_projection_value(e: &Expr, ctx: &EvalContext<'_>) -> Option<Value<'static>> {
1844 if matches!(e, Expr::Literal(_)) || !constant_expr(e) {
1845 return None;
1846 }
1847 eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx).ok()
1848}
1849
1850/// v7.39 (round 597) — an expression whose value cannot depend on the row.
1851/// An allowlist of node kinds, for the reason rounds 590 and 596 recorded:
1852/// asking "does it mention a column" would admit a node the walk did not
1853/// know about, and a function whose volatility SPG cannot look up.
1854///
1855/// r1043 — there is a lookup now, so a call to a function on
1856/// [`crate::immutable_fn`]'s positive list joins the list. Round 605's
1857/// own note recorded `upper('abc')` as the one it could not fold; the
1858/// gate it was missing was never the node kind, it was the volatility.
1859/// A name that has not been checked against `pg_proc` is still refused,
1860/// so the failure mode stays "not folded" rather than "folded wrongly".
1861pub(crate) fn constant_expr(e: &Expr) -> bool {
1862 match e {
1863 Expr::Literal(_) => true,
1864 Expr::Array(items) => items.iter().all(constant_expr),
1865 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => constant_expr(expr),
1866 Expr::Binary { lhs, rhs, .. } => constant_expr(lhs) && constant_expr(rhs),
1867 Expr::FunctionCall { name, args } => {
1868 crate::immutable_fn::is_immutable_builtin(name) && args.iter().all(constant_expr)
1869 }
1870 _ => false,
1871 }
1872}
1873
1874/// v7.39 (round 595) — the source sub-expression of the EXTRACT node a
1875/// `Step::Extract` was compiled from. Only its declared TYPE is read, for
1876/// the error wording; the value came off the stack.
1877fn source_of_extract(node: &Expr) -> &Expr {
1878 match node {
1879 Expr::Extract { source, .. } => source,
1880 other => other,
1881 }
1882}
1883
1884/// v7.39 (round 594) — the literal pattern and case flag of a `regexp_like`
1885/// call, when both are literals. `None` keeps the call on the interpreter.
1886fn regex_literal_parts(args: &[Expr]) -> Option<(&str, bool)> {
1887 let Expr::Literal(spg_sql::ast::Literal::String(pat)) = &args[1] else {
1888 return None;
1889 };
1890 let ci = match args.get(2) {
1891 None => false,
1892 Some(Expr::Literal(spg_sql::ast::Literal::String(f))) => f.contains('i'),
1893 Some(_) => return None,
1894 };
1895 Some((pat.as_str(), ci))
1896}
1897
1898/// The verdict `Step::Regex` produces. `None` means the operand is not text,
1899/// which is the caller's cue to fall through to the interpreter for its own
1900/// coercion and wording.
1901fn regex_verdict(
1902 cell: &Value<'_>,
1903 re: &crate::eval::CompiledRe,
1904) -> Option<Result<Value<'static>, EvalError>> {
1905 let text = match cell {
1906 Value::Null => return Some(Ok(Value::Null)),
1907 Value::Text(t) | Value::BpChar(t) => t.as_ref(),
1908 _ => return None,
1909 };
1910 Some(crate::eval::compiled_is_match(re, text).map(Value::Bool))
1911}
1912
1913/// v7.39 (round 488) — the verdict `Step::Like` / `Step::LikeSubstring`
1914/// produce, restated for the fast predicate. `None` means the operand is
1915/// not text, which is the caller's cue to fall through to the VM and let
1916/// it raise the type error in its own wording.
1917///
1918/// v7.39 (round 489) — the VM arm calls this too, so there is one body
1919/// rather than two that can drift. Round 488 kept them separate on round
1920/// 486's belief that editing that loop costs unrelated shapes; round 489
1921/// re-measured that belief with the shapes isolated and force-inlined the
1922/// helper, and the cost is gone (see `in_set_verdict`).
1923/// `e2e_like_fast_path_round488` runs both entry points over the same
1924/// matrix.
1925#[allow(clippy::inline_always)] // measured: see `in_set_verdict`
1926#[inline(always)]
1927fn like_verdict(cell: &Value<'_>, step: &Step) -> Option<Result<Value<'static>, EvalError>> {
1928 let (text, negated) = match (cell, step) {
1929 (Value::Null, _) => return Some(Ok(Value::Null)),
1930 (
1931 Value::Text(t) | Value::BpChar(t),
1932 Step::Like { negated, .. } | Step::LikeSubstring { negated, .. },
1933 ) => (t.as_ref(), *negated),
1934 _ => return None,
1935 };
1936 let matched = match step {
1937 Step::Like {
1938 pattern,
1939 case_insensitive,
1940 ..
1941 } => {
1942 let r = if *case_insensitive {
1943 like_match_str(&text.to_lowercase(), pattern, 0)
1944 } else {
1945 like_match_str(text, pattern, 0)
1946 };
1947 match r {
1948 Ok(m) => m,
1949 Err(e) => return Some(Err(e)),
1950 }
1951 }
1952 Step::LikeSubstring {
1953 needle,
1954 k_before,
1955 m_after,
1956 case_insensitive,
1957 ..
1958 } => {
1959 if *case_insensitive {
1960 like_substring_match(&text.to_lowercase(), needle, *k_before, *m_after)
1961 } else {
1962 like_substring_match(text, needle, *k_before, *m_after)
1963 }
1964 }
1965 _ => return None,
1966 };
1967 Some(Ok(Value::Bool(if negated { !matched } else { matched })))
1968}
1969
1970/// Return an emptied stack's allocation with its value lifetime reset.
1971/// This is the standard "recycle" pattern (cf. the `recycle_vec` crate):
1972/// an EMPTY `Vec<Value<'a>>` holds no values, only a raw allocation, so
1973/// re-labelling its element lifetime cannot dangle.
1974#[allow(unsafe_code)] // empty-Vec lifetime relabel; isolated (see SAFETY).
1975fn recycle_stack(mut v: Vec<Value<'_>>) -> Vec<Value<'static>> {
1976 // v7.39 (round 481) — read before the clear: this is exactly the set of
1977 // values the clear is about to drop.
1978 crate::bump_counter!(STEP_VM_STACK_LEFTOVER, v.len() as u64);
1979 #[cfg(feature = "perf-counters")]
1980 {
1981 let heap = v
1982 .iter()
1983 .filter(|x| {
1984 matches!(
1985 x,
1986 Value::Text(_) | Value::Bytes(_) | Value::Json(_) | Value::Vector(_)
1987 )
1988 })
1989 .count();
1990 crate::bump_counter!(STEP_VM_STACK_LEFTOVER_HEAP, heap as u64);
1991 }
1992 v.clear();
1993 debug_assert!(v.is_empty());
1994 // SAFETY: `v` is empty (cleared above) — there are no `Value<'_>`s
1995 // whose lifetime could be unsoundly extended; `Vec<Value<'a>>` and
1996 // `Vec<Value<'static>>` are the same type constructor differing only
1997 // in a lifetime parameter, so they have identical size/align/layout
1998 // (lifetimes are erased before layout is computed).
1999 unsafe { core::mem::transmute::<Vec<Value<'_>>, Vec<Value<'static>>>(v) }
2000}
2001
2002/// v7.32 (P4 borrow channel, increment 2) — the RowRef-borrowing form of
2003/// `eval_compiled`. `Step::Column` borrows its cell straight from the
2004/// RowRef (a join tuple resolves it via `tuple_value`, never
2005/// materialising a combined Row); only the rare Subtree / InSet
2006/// cross-family fallback materialises the row once. Bit-for-bit
2007/// equivalent to the Owned path — `eval_compiled` above is now a thin
2008/// `RowRef::Owned` wrapper, so there is a single interpreter (invariant
2009/// I3); a differential test pins the equivalence.
2010// v7.37.9 T3 S1 — row-lifetime stack plumbing. Two lifetimes:
2011// `'row` = the RowRef's data lifetime; `'val` = stack value lifetime
2012// (must outlive function return). Constraint `'row: 'val` allows the
2013// step body to push `Value::Text(Cow::Borrowed(row_cell))` (S2+) while
2014// the caller's stack stays at whatever lifetime it declared (often
2015// `'static` for Vec<Value<'static>>). S1 keeps every step body forcing
2016// `.into_owned()` so behaviour is bit-identical; later stages
2017// (S2 Column, S3 Lit, S4 Binary, S6 Function, S7 Case) progressively
2018// switch to borrowed push to eliminate per-row String allocs.
2019pub(crate) fn eval_compiled_ref<'row, 'val>(
2020 c: &'val CompiledExpr,
2021 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
2022 // borrows the row data, not the wrapper, so taking a reference here
2023 // only served to tie the result's lifetime to a caller local — which
2024 // is what stopped the aggregate loop from holding its `RowRef` by
2025 // value and forced a materialised `Vec<RowRef>` per scan.
2026 row: crate::join::RowRef<'row>,
2027 ctx: &EvalContext<'_>,
2028 stack: &mut Vec<Value<'val>>,
2029) -> Result<Value<'val>, EvalError>
2030where
2031 'row: 'val,
2032{
2033 stack.clear();
2034 run_compiled_steps(&c.steps, row, ctx, stack)?;
2035 Ok(stack.pop().unwrap_or(Value::Null))
2036}
2037
2038/// v7.37.5-A2b — append-mode entry point for nested sub-programs (the
2039/// `Step::Case` executor's per-branch evaluations). Does NOT clear the
2040/// stack; pushes the program's result on top of whatever was already
2041/// there. Caller uses the `mark` to know where to truncate / pop. Kept
2042/// out of public surface — only the Case opcode reaches for it.
2043fn eval_compiled_ref_into<'row, 'val>(
2044 c: &'val CompiledExpr,
2045 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
2046 // borrows the row data, not the wrapper, so taking a reference here
2047 // only served to tie the result's lifetime to a caller local — which
2048 // is what stopped the aggregate loop from holding its `RowRef` by
2049 // value and forced a materialised `Vec<RowRef>` per scan.
2050 row: crate::join::RowRef<'row>,
2051 ctx: &EvalContext<'_>,
2052 stack: &mut Vec<Value<'val>>,
2053 _mark: usize,
2054) -> Result<(), EvalError>
2055where
2056 'row: 'val,
2057{
2058 run_compiled_steps(&c.steps, row, ctx, stack)
2059}
2060
2061#[inline]
2062fn run_compiled_steps<'row, 'val>(
2063 steps: &'val [Step],
2064 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
2065 // borrows the row data, not the wrapper, so taking a reference here
2066 // only served to tie the result's lifetime to a caller local — which
2067 // is what stopped the aggregate loop from holding its `RowRef` by
2068 // value and forced a materialised `Vec<RowRef>` per scan.
2069 row: crate::join::RowRef<'row>,
2070 ctx: &EvalContext<'_>,
2071 stack: &mut Vec<Value<'val>>,
2072) -> Result<(), EvalError>
2073where
2074 'row: 'val,
2075{
2076 // v7.37.9 Phase 1A-ext-2 T1 — counter per call into the Step VM
2077 // interpreter. Tells us "how many steps does the average compiled
2078 // arg run per row" → narrows the attack target (subtree CSE vs
2079 // column-ref-push vs multi-spec combine). Read-only.
2080 crate::bump_counter!(STEP_VM_CALL_COUNT);
2081 crate::bump_counter!(STEP_VM_STEPS_TOTAL, steps.len() as u64);
2082 for step in steps {
2083 match step {
2084 Step::Column(pos) => {
2085 crate::bump_counter!(STEP_VM_COLUMN_FIRE);
2086 // v7.37.9 T3 S2 — catalog rows hold `Cow::Owned(String)`
2087 // for Text-class variants (per `spg-storage/src/lib.rs:539`
2088 // — "Persistent / catalog Values use Value<'static> with
2089 // Cow::Owned(...)"). Plain `.clone()` would therefore
2090 // still trigger `String::clone()` per cell read. Instead
2091 // manually wrap the existing storage into a borrowed Cow
2092 // pointing at the same bytes — zero-alloc push.
2093 let cell: Value<'val> = match row.get(*pos) {
2094 Some(spg_storage::Value::Text(s)) => {
2095 spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
2096 }
2097 Some(spg_storage::Value::Bytes(b)) => {
2098 spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
2099 }
2100 Some(spg_storage::Value::Json(s)) => {
2101 spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
2102 }
2103 Some(spg_storage::Value::Vector(v)) => {
2104 spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(v.as_ref()))
2105 }
2106 // Copy-light variants: clone is free (just enum copy).
2107 Some(v) => v.clone(),
2108 None => Value::Null,
2109 };
2110 // Classification counter unchanged (still counts cells
2111 // that WERE heap-bearing in the baseline).
2112 if matches!(
2113 &cell,
2114 spg_storage::Value::Text(_)
2115 | spg_storage::Value::Bytes(_)
2116 | spg_storage::Value::Json(_)
2117 | spg_storage::Value::Vector(_)
2118 ) {
2119 crate::bump_counter!(STEP_VM_COLUMN_HEAP_ALLOC);
2120 }
2121 stack.push(cell);
2122 }
2123 Step::Lit(v) => {
2124 crate::bump_counter!(STEP_VM_LIT_FIRE);
2125 if matches!(
2126 v,
2127 spg_storage::Value::Text(_)
2128 | spg_storage::Value::Bytes(_)
2129 | spg_storage::Value::Json(_)
2130 | spg_storage::Value::Vector(_)
2131 ) {
2132 crate::bump_counter!(STEP_VM_LIT_HEAP_ALLOC);
2133 }
2134 // v7.37.9 T3 S3 — borrow literal storage instead of
2135 // String::clone'ing it. Step variants own their
2136 // literal (`Value<'static>` enum payload), so we can
2137 // safely construct a `Cow::Borrowed(&'static …)` view.
2138 // Same pattern as S2's Column path.
2139 let pushed: Value<'val> = match v {
2140 spg_storage::Value::Text(s) => {
2141 spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
2142 }
2143 spg_storage::Value::Bytes(b) => {
2144 spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
2145 }
2146 spg_storage::Value::Json(s) => {
2147 spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
2148 }
2149 spg_storage::Value::Vector(vec) => {
2150 spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(vec.as_ref()))
2151 }
2152 other => other.clone(),
2153 };
2154 stack.push(pushed);
2155 }
2156 Step::Binary(op) => {
2157 crate::bump_counter!(STEP_VM_BINARY_FIRE);
2158 // v7.37.9 T3 S4 — try the by-ref fast path first
2159 // (comparison + 3VL ops). For those, operand bytes are
2160 // read but never stored in the result; we avoid the
2161 // .into_owned() that would clone every Cow::Borrowed
2162 // Text/Bytes/Json/Vector pushed by S2/S3. For ops that
2163 // build owned results (arithmetic, concat, json get,
2164 // etc.) apply_binary_by_ref returns None and we fall
2165 // through to the owning path.
2166 // v7.39 (round 346, M1) — the MySQL reading of AND / OR
2167 // has to be here TOO: a compiled predicate never passes
2168 // through `eval_expr`'s arm, so `WHERE a AND 1` still
2169 // errored on a MySQL session while the interpreted form
2170 // answered. (The pin found this, not the reading.)
2171 if ctx.mysql_dialect && matches!(op, BinOp::And | BinOp::Or) {
2172 let r = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
2173 let l = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
2174 stack.push(apply_binary(*op, l, r)?);
2175 continue;
2176 }
2177 let n = stack.len();
2178 if n >= 2 {
2179 if let Some(result) =
2180 super::apply_binary_by_ref(*op, &stack[n - 2], &stack[n - 1])?
2181 {
2182 stack.truncate(n - 2);
2183 stack.push(result);
2184 continue;
2185 }
2186 }
2187 let r = stack.pop().unwrap_or(Value::Null).into_owned();
2188 let l = stack.pop().unwrap_or(Value::Null).into_owned();
2189 stack.push(apply_binary(*op, l, r)?);
2190 }
2191 Step::Connective { op, rhs } => {
2192 crate::bump_counter!(STEP_VM_BINARY_FIRE);
2193 let l = stack.pop().unwrap_or(Value::Null).into_owned();
2194 // The left decides, or it does not. A NULL decides nothing:
2195 // NULL AND false is false, so the right side is still needed.
2196 match (op, &l) {
2197 (BinOp::And, Value::Bool(false)) => {
2198 stack.push(Value::Bool(false));
2199 continue;
2200 }
2201 (BinOp::Or, Value::Bool(true)) => {
2202 stack.push(Value::Bool(true));
2203 continue;
2204 }
2205 _ => {}
2206 }
2207 run_compiled_steps(rhs, row, ctx, stack)?;
2208 let r = stack.pop().unwrap_or(Value::Null).into_owned();
2209 stack.push(apply_binary(*op, l, r)?);
2210 }
2211 Step::BinaryCi(op) => {
2212 // v7.39 (round 364, M4 P2) — the MySQL session uses the
2213 // accent-aware fold; a PG `case_insensitive` column keeps
2214 // its ASCII-only contract.
2215 let fold = |v: Value<'static>| match v {
2216 // v7.38.16 — BpChar as well. `CASE` a few hundred
2217 // lines below already had the pair; this arm, which
2218 // is where `s >= 'DELTA'` and BETWEEN land, did not,
2219 // so a CHAR column compared by bytes AND kept its
2220 // padding.
2221 // v7.38.17 — CHAR pads, TEXT does not.
2222 Value::BpChar(s) if ctx.mysql_dialect => {
2223 Value::text(spg_storage::mysql_compare_fold_char(&s))
2224 }
2225 Value::Text(s) if ctx.mysql_dialect => {
2226 Value::text(spg_storage::mysql_compare_fold(&s))
2227 }
2228 Value::Text(s) | Value::BpChar(s) => Value::text(s.to_ascii_lowercase()),
2229 other => other,
2230 };
2231 let r = fold(stack.pop().unwrap_or(Value::Null).into_owned());
2232 let l = fold(stack.pop().unwrap_or(Value::Null).into_owned());
2233 stack.push(apply_binary(*op, l, r)?);
2234 }
2235 Step::Unary(op) => {
2236 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2237 if ctx.mysql_dialect
2238 && matches!(op, UnOp::Not)
2239 && !matches!(v, Value::Bool(_) | Value::Null)
2240 {
2241 stack.push(Value::Bool(!super::predicate_is_true(&v, "NOT", true)?));
2242 continue;
2243 }
2244 stack.push(apply_unary(*op, v)?);
2245 }
2246 Step::IsNull { negated } => {
2247 let v = stack.pop().unwrap_or(Value::Null);
2248 let is_null = matches!(v, Value::Null);
2249 stack.push(Value::Bool(if *negated { !is_null } else { is_null }));
2250 }
2251 Step::AnyTextMatch { negated } => {
2252 let v = stack.pop().unwrap_or(Value::Null);
2253 stack.push(match v {
2254 Value::Null => Value::Null,
2255 _ => Value::Bool(!*negated),
2256 });
2257 }
2258 Step::InSet {
2259 set,
2260 has_null,
2261 negated,
2262 fallback,
2263 } => {
2264 let needle = stack.pop().unwrap_or(Value::Null);
2265 match in_set_verdict(&needle, set, *has_null, *negated) {
2266 Some(v) => stack.push(v),
2267 // Cross-family needle: take the interpreter's
2268 // exact coercion / error path on the whole node.
2269 None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
2270 }
2271 }
2272 Step::AnyAll { op, is_any, arr } => {
2273 let lhs = stack.pop().unwrap_or(Value::Null).into_owned();
2274 stack.push(crate::eval::any_all_over(lhs, arr.clone(), op, *is_any)?);
2275 }
2276 Step::Extract { field, fallback } => {
2277 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2278 stack.push(crate::eval::extract_from_value(
2279 field,
2280 v,
2281 source_of_extract(fallback),
2282 ctx,
2283 )?);
2284 }
2285 Step::Regex { re, fallback } => {
2286 let v = stack.pop().unwrap_or(Value::Null);
2287 match regex_verdict(&v, re) {
2288 Some(r) => stack.push(r?),
2289 // Not text: the interpreter's coercion and wording, on
2290 // the whole node, exactly as `Step::InSet` does.
2291 None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
2292 }
2293 }
2294 step @ (Step::Like { .. } | Step::LikeSubstring { .. }) => {
2295 // v7.39 (round 489) — one arm for both pattern steps,
2296 // sharing `like_verdict` with the fast predicate.
2297 //
2298 // The matching itself was already out of line: v7.37.16
2299 // borrowed the operand instead of paying `.into_owned()`
2300 // plus a per-row `Vec<char>` collect (~90 ns/row of
2301 // allocator traffic on a LIKE table scan), and round 484
2302 // replaced `str::find`'s two-way searcher — whose SETUP
2303 // was 14.6 % of self time, rebuilt every row for a
2304 // two-byte constant needle — with an ASCII byte scan.
2305 // ILIKE still lowercases; plain LIKE allocates nothing.
2306 let v = stack.pop().unwrap_or(Value::Null);
2307 match like_verdict(&v, step) {
2308 Some(r) => stack.push(r?),
2309 None => {
2310 return Err(EvalError::TypeMismatch {
2311 detail: format!(
2312 "LIKE requires text operands, got {}",
2313 crate::conversions::pg_type_name_for_error_opt(v.data_type())
2314 ),
2315 });
2316 }
2317 }
2318 }
2319 Step::ColumnLength { pos } => {
2320 // v7.36 — zero-copy LENGTH on a column. Read the
2321 // cell by reference; compute char count without
2322 // cloning the underlying `String`. Saves 25 k ×
2323 // ~1 KB heap clones on the user_storage_usage shape.
2324 let v = row.get(*pos).unwrap_or(&Value::Null);
2325 let pushed = match v {
2326 Value::Null => Value::Null,
2327 Value::Text(s) => {
2328 let n = if s.is_ascii() {
2329 i32::try_from(s.len()).unwrap_or(i32::MAX)
2330 } else {
2331 i32::try_from(s.chars().count()).unwrap_or(i32::MAX)
2332 };
2333 Value::Int(n)
2334 }
2335 // v7.39 (bpchar epic) — length(bpchar) counts with the
2336 // trailing blanks stripped (length('ab'::char(5)) = 2).
2337 Value::BpChar(s) => {
2338 let t = s.trim_end_matches(' ');
2339 let n = if t.is_ascii() {
2340 i32::try_from(t.len()).unwrap_or(i32::MAX)
2341 } else {
2342 i32::try_from(t.chars().count()).unwrap_or(i32::MAX)
2343 };
2344 Value::Int(n)
2345 }
2346 Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
2347 other => {
2348 return Err(EvalError::TypeMismatch {
2349 detail: format!(
2350 "length() needs text or bytea, got {}",
2351 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2352 ),
2353 });
2354 }
2355 };
2356 stack.push(pushed);
2357 }
2358 Step::ColumnOctetLength { pos } => {
2359 let v = row.get(*pos).unwrap_or(&Value::Null);
2360 let pushed = match v {
2361 Value::Null => Value::Null,
2362 // v7.39 (bpchar epic) — octet_length(bpchar) counts the
2363 // PADDED stored form.
2364 Value::Text(s) | Value::BpChar(s) => {
2365 Value::Int(i32::try_from(s.len()).unwrap_or(i32::MAX))
2366 }
2367 Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
2368 other => {
2369 return Err(EvalError::TypeMismatch {
2370 detail: format!(
2371 "octet_length() needs text or bytea, got {}",
2372 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2373 ),
2374 });
2375 }
2376 };
2377 stack.push(pushed);
2378 }
2379 Step::Function { name_lower, n_args } => {
2380 crate::bump_counter!(STEP_VM_FUNCTION_FIRE);
2381 let start = stack.len().saturating_sub(*n_args);
2382 // `apply_function` borrows the trailing `n_args`
2383 // values off the stack; we then truncate + push the
2384 // result. `name_lower` is pre-lowercased at compile
2385 // time, so dispatch skips the per-row
2386 // `to_ascii_lowercase()` allocation.
2387 // v7.37.9 T3 S6 — apply_function_lower signature relaxed
2388 // to `&[Value<'_>]`; pass the borrowed stack slice
2389 // directly. Eliminates the Vec materialise + per-arg
2390 // String::clone that S1 introduced as a placeholder.
2391 let result =
2392 super::functions::apply_function_lower(name_lower, &stack[start..], ctx)?;
2393 stack.truncate(start);
2394 stack.push(result);
2395 }
2396 Step::Coalesce { n_args } => {
2397 let start = stack.len().saturating_sub(*n_args);
2398 // The widening `COALESCE(1, 2.5)` needs only exists when the
2399 // non-null arguments carry MIXED types; inspected by ref, and
2400 // the mixed shapes fall to the owned arm that always did it.
2401 let mut mixed = false;
2402 let mut seen: Option<spg_storage::DataType> = None;
2403 for v in &stack[start..] {
2404 if let Some(t) = v.data_type() {
2405 match seen {
2406 None => seen = Some(t),
2407 Some(prev) if prev != t => {
2408 mixed = true;
2409 break;
2410 }
2411 Some(_) => {}
2412 }
2413 }
2414 }
2415 if mixed {
2416 let result =
2417 super::functions::apply_function_lower("coalesce", &stack[start..], ctx)?;
2418 stack.truncate(start);
2419 stack.push(result);
2420 } else {
2421 let chosen = stack[start..]
2422 .iter()
2423 .position(|v| !matches!(v, Value::Null));
2424 match chosen {
2425 Some(k) => {
2426 let v = stack.swap_remove(start + k);
2427 stack.truncate(start);
2428 stack.push(v);
2429 }
2430 None => {
2431 stack.truncate(start);
2432 stack.push(Value::Null);
2433 }
2434 }
2435 }
2436 }
2437 Step::Extremum { n_args, max } => {
2438 let start = stack.len().saturating_sub(*n_args);
2439 // Fast path: every non-NULL argument carries the SAME
2440 // concrete type — the comparison is the type's own and
2441 // the widen-to-common finish is the identity. Everything
2442 // else (mixed types, unknown-type text beside a typed
2443 // sibling, xid's refusal, MySQL's NULL-poisoning) falls
2444 // to the function arm unchanged.
2445 let mut uniform: Option<spg_storage::DataType> = None;
2446 let mut any_null = false;
2447 let mut fall_back = false;
2448 for v in &stack[start..] {
2449 if matches!(v, Value::Null) {
2450 any_null = true;
2451 continue;
2452 }
2453 if matches!(v, Value::Xid(_)) {
2454 fall_back = true;
2455 break;
2456 }
2457 match (v.data_type(), uniform) {
2458 (Some(t), None) => uniform = Some(t),
2459 (Some(t), Some(prev)) if t != prev => {
2460 fall_back = true;
2461 break;
2462 }
2463 (Some(_), Some(_)) => {}
2464 (None, _) => {
2465 fall_back = true;
2466 break;
2467 }
2468 }
2469 }
2470 if fall_back || (ctx.mysql_dialect && any_null) {
2471 let name = if *max { "greatest" } else { "least" };
2472 let result =
2473 super::functions::apply_function_lower(name, &stack[start..], ctx)?;
2474 stack.truncate(start);
2475 stack.push(result);
2476 } else {
2477 let mut best: Option<usize> = None;
2478 for k in start..stack.len() {
2479 if matches!(&stack[k], Value::Null) {
2480 continue;
2481 }
2482 match best {
2483 None => best = Some(k),
2484 Some(b) => {
2485 let ord = super::values::value_cmp_for_min_max(
2486 &stack[b],
2487 &stack[k],
2488 ctx.mysql_dialect,
2489 );
2490 let take = if *max {
2491 ord == core::cmp::Ordering::Less
2492 } else {
2493 ord == core::cmp::Ordering::Greater
2494 };
2495 if take {
2496 best = Some(k);
2497 }
2498 }
2499 }
2500 }
2501 match best {
2502 Some(k) => {
2503 let v = stack.swap_remove(k);
2504 stack.truncate(start);
2505 stack.push(v);
2506 }
2507 None => {
2508 stack.truncate(start);
2509 stack.push(Value::Null);
2510 }
2511 }
2512 }
2513 }
2514 Step::NullIf => {
2515 let n = stack.len();
2516 // NULLIF is `=` under the hood and keeps round 238's refusal
2517 // of incomparable operands; both reads are by reference.
2518 let verdict = match (&stack[n - 2], &stack[n - 1]) {
2519 (Value::Null, _) => Some(true),
2520 (_, Value::Null) => Some(false),
2521 (a, b) => {
2522 super::binop::require_comparable(spg_sql::ast::BinOp::Eq, a, b)?;
2523 match super::apply_binary_by_ref(spg_sql::ast::BinOp::Eq, a, b)? {
2524 Some(Value::Bool(eq)) => Some(eq),
2525 _ => None,
2526 }
2527 }
2528 };
2529 match verdict {
2530 Some(true) => {
2531 stack.truncate(n - 2);
2532 stack.push(Value::Null);
2533 }
2534 Some(false) => {
2535 let a = stack.swap_remove(n - 2);
2536 stack.truncate(n - 2);
2537 stack.push(a);
2538 }
2539 // The by-ref compare could not decide — the owned arm can.
2540 None => {
2541 let result =
2542 super::functions::apply_function_lower("nullif", &stack[n - 2..], ctx)?;
2543 stack.truncate(n - 2);
2544 stack.push(result);
2545 }
2546 }
2547 }
2548 Step::Cast { target } => {
2549 crate::bump_counter!(STEP_VM_CAST_FIRE);
2550 // v7.39 (round 621) — two allocations a row lived on this one
2551 // line: `into_owned()` cloned a borrowed text cell just to
2552 // hand it to the cast, and `target.clone()` re-built the
2553 // target (a String, for the Named form) EVERY row even though
2554 // it is a compile product. `count(s::TEXT)` — a cast that
2555 // changes nothing — measured 2.00 allocs/row and 12 ms where
2556 // `count(s)` measures 0.00 and 2.8 ms.
2557 //
2558 // A cast that is an identity on the value it was given hands
2559 // the borrowed value straight back; everything else takes the
2560 // owned path, with the target passed by reference.
2561 let v = stack.pop().unwrap_or(Value::Null);
2562 if cast_is_identity_for(&v, target) {
2563 stack.push(v);
2564 } else {
2565 stack.push(super::cast::cast_value_ref_in(
2566 v.into_owned(),
2567 target,
2568 ctx.mysql_dialect,
2569 )?);
2570 }
2571 }
2572 Step::CastPlain { dt, name } => {
2573 let v = stack.pop().unwrap_or(Value::Null);
2574 // The name is pre-validated (it came off the plain table),
2575 // so NULL keeps its short-circuit; a same-type value passes
2576 // through untouched, exactly the identity the Cast step
2577 // recognises.
2578 let identity = matches!(
2579 (&v, dt),
2580 (Value::Null, _)
2581 | (Value::Int(_), spg_storage::DataType::Int)
2582 | (Value::BigInt(_), spg_storage::DataType::BigInt)
2583 | (Value::SmallInt(_), spg_storage::DataType::SmallInt)
2584 | (Value::Real(_), spg_storage::DataType::Real)
2585 | (Value::Float(_), spg_storage::DataType::Float)
2586 | (Value::Bool(_), spg_storage::DataType::Bool)
2587 | (Value::Date(_), spg_storage::DataType::Date)
2588 | (Value::Uuid(_), spg_storage::DataType::Uuid)
2589 );
2590 if identity {
2591 stack.push(v);
2592 } else {
2593 stack.push(super::cast::finish_named_cast_plain(
2594 v.into_owned(),
2595 *dt,
2596 name,
2597 ctx.mysql_dialect,
2598 )?);
2599 }
2600 }
2601 Step::Case {
2602 fold_operand,
2603 operand,
2604 branches,
2605 else_branch,
2606 } => {
2607 crate::bump_counter!(STEP_VM_CASE_FIRE);
2608 // v7.37.5-A2b — short-circuit Case executor. Mirrors
2609 // `Expr::Case` interpreter semantics bit-for-bit (each
2610 // WHEN evaluates with its own scratch stack; first
2611 // match wins; ELSE = NULL when absent). The outer
2612 // `stack` is reused (truncated back to its pre-Case
2613 // mark after each sub-program); allocator-free per
2614 // branch — the prior version allocated a fresh
2615 // `Vec<Value>` per sub-program which showed up as
2616 // ~3 % `drop_in_place<Vec<Value>>` self time.
2617 let mark = stack.len();
2618 // v7.37.9 T3 S7 — Case sub-program lifetime threads
2619 // through naturally via S1's `'row: 'val`. Operand /
2620 // when / matched / else results are pushed by sub-progs
2621 // into our same stack; we pop them as `Value<'val>` and
2622 // keep them at that lifetime instead of forcing
2623 // into_owned. The simple-form operand match (Eq) uses
2624 // apply_binary_by_ref to avoid the operand clone +
2625 // pop-side into_owned the S1 placeholder was paying.
2626 let operand_value: Option<Value<'val>> = if let Some(op) = operand {
2627 eval_compiled_ref_into(op, row, ctx, stack, mark)?;
2628 Some(stack.pop().unwrap_or(Value::Null))
2629 } else {
2630 None
2631 };
2632 stack.truncate(mark);
2633 let mut matched_value: Option<Value<'val>> = None;
2634 for (when_c, then_c) in branches {
2635 eval_compiled_ref_into(when_c, row, ctx, stack, mark)?;
2636 let when_v = stack.pop().unwrap_or(Value::Null);
2637 stack.truncate(mark);
2638 let matched = match &operand_value {
2639 None => matches!(when_v, Value::Bool(true)),
2640 Some(op_v) => {
2641 // Try the by-ref comparison fast path; fall
2642 // back to owning apply_binary only if the
2643 // by-ref path returns None (non-comparison
2644 // op, which Eq never is).
2645 // v7.38.14 — fold first when the compile-time
2646 // decision says this arm's collation folds.
2647 // `apply_binary_by_ref` compares bytes and knows
2648 // nothing about a dialect, which is why
2649 // `CASE s WHEN 'A'` missed 'a' on a
2650 // case-insensitive column.
2651 let folded = if *fold_operand {
2652 match (op_v, &when_v) {
2653 // v7.38.17 — CHAR pads, TEXT does not.
2654 (Value::BpChar(x), Value::BpChar(y)) => Some((
2655 Value::text(spg_storage::mysql_compare_fold_char(x)),
2656 Value::text(spg_storage::mysql_compare_fold_char(y)),
2657 )),
2658 (Value::Text(x), Value::Text(y)) => Some((
2659 Value::text(spg_storage::mysql_compare_fold(x)),
2660 Value::text(spg_storage::mysql_compare_fold(y)),
2661 )),
2662 _ => None,
2663 }
2664 } else {
2665 None
2666 };
2667 let eq_result = match folded {
2668 Some((l, r)) => apply_binary(BinOp::Eq, l, r)?,
2669 None => match super::apply_binary_by_ref(BinOp::Eq, op_v, &when_v)?
2670 {
2671 Some(v) => v,
2672 None => apply_binary(
2673 BinOp::Eq,
2674 op_v.clone().into_owned(),
2675 when_v.clone().into_owned(),
2676 )?,
2677 },
2678 };
2679 matches!(eq_result, Value::Bool(true))
2680 }
2681 };
2682 if matched {
2683 eval_compiled_ref_into(then_c, row, ctx, stack, mark)?;
2684 matched_value = Some(stack.pop().unwrap_or(Value::Null));
2685 stack.truncate(mark);
2686 break;
2687 }
2688 }
2689 let v: Value<'val> = match matched_value {
2690 Some(v) => v,
2691 None => match else_branch {
2692 Some(el) => {
2693 eval_compiled_ref_into(el, row, ctx, stack, mark)?;
2694 let v = stack.pop().unwrap_or(Value::Null);
2695 stack.truncate(mark);
2696 v
2697 }
2698 None => Value::Null,
2699 },
2700 };
2701 stack.push(v);
2702 }
2703 Step::CoerceCommon(target) => {
2704 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2705 stack.push(super::widen_value_to(v, *target));
2706 }
2707 Step::Subtree(e) => stack.push(eval_expr(e, &row.as_row(), ctx)?),
2708 }
2709 }
2710 Ok(())
2711}
2712
2713/// v7.37.9 Phase 1A-ext-2 T1 — Step VM internal step-type counters.
2714/// Read-only diagnostic; gates no behaviour. Used by counter_dump.rs
2715/// to ground-truth subtree CSE / column-ref-push / multi-spec-combine
2716/// attack ROI estimates.
2717pub static STEP_VM_CALL_COUNT: core::sync::atomic::AtomicU64 =
2718 core::sync::atomic::AtomicU64::new(0);
2719pub static STEP_VM_STEPS_TOTAL: core::sync::atomic::AtomicU64 =
2720 core::sync::atomic::AtomicU64::new(0);
2721pub static STEP_VM_COLUMN_FIRE: core::sync::atomic::AtomicU64 =
2722 core::sync::atomic::AtomicU64::new(0);
2723pub static STEP_VM_LIT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2724pub static STEP_VM_BINARY_FIRE: core::sync::atomic::AtomicU64 =
2725 core::sync::atomic::AtomicU64::new(0);
2726pub static STEP_VM_FUNCTION_FIRE: core::sync::atomic::AtomicU64 =
2727 core::sync::atomic::AtomicU64::new(0);
2728pub static STEP_VM_CAST_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2729pub static STEP_VM_CASE_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2730
2731/// v7.37.9 Round 3 — heap-alloc counters specifically for the T3
2732/// structural attack's ROI estimate. Step::Column / Step::Lit hits
2733/// pay a String alloc when the cell variant is heap-bearing
2734/// (Text/Bytes/Json/Vector). T3 stack-lifetime push-by-borrow
2735/// would eliminate these for the bulk of per-row work.
2736pub static STEP_VM_COLUMN_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
2737 core::sync::atomic::AtomicU64::new(0);
2738pub static STEP_VM_LIT_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
2739 core::sync::atomic::AtomicU64::new(0);
2740
2741/// v7.39 (round 481) — how many values the stack still holds when a call
2742/// finishes, and how many are heap-bearing.
2743///
2744/// Round 480 left `drop_glue<Value>` at 16 % of self time with the drops
2745/// attributed to the predicate closure, i.e. to the stack rather than to
2746/// the returned value (round 479 removed that one). Whether the ops leave
2747/// operands behind for the next call's `clear()` to drop is a question
2748/// with a number, so this counts it rather than reasoning about it — the
2749/// previous round was spent acting on an inference that turned out to name
2750/// an unreachable branch.
2751/// v7.39 (round 482) — how often the `<column> <cmp> <literal>` fast
2752/// predicate fires, so "is it even reached" is a number and not a guess
2753/// (round 480 was spent on a branch that turned out to be unreachable).
2754pub static STEP_VM_FASTPRED_FIRE: core::sync::atomic::AtomicU64 =
2755 core::sync::atomic::AtomicU64::new(0);
2756
2757/// r1021 — how often the integer lane answers, and how often a row makes it
2758/// hand back. Round 480 was spent on a branch that turned out unreachable,
2759/// so "is it even reached" stays a number here too — and the fallback
2760/// counter is the one that matters for correctness review: it is every row
2761/// the lane declined to decide.
2762pub static STEP_VM_INTLANE_FIRE: core::sync::atomic::AtomicU64 =
2763 core::sync::atomic::AtomicU64::new(0);
2764pub static STEP_VM_INTLANE_FALLBACK: core::sync::atomic::AtomicU64 =
2765 core::sync::atomic::AtomicU64::new(0);
2766
2767pub static STEP_VM_STACK_LEFTOVER: core::sync::atomic::AtomicU64 =
2768 core::sync::atomic::AtomicU64::new(0);
2769pub static STEP_VM_STACK_LEFTOVER_HEAP: core::sync::atomic::AtomicU64 =
2770 core::sync::atomic::AtomicU64::new(0);
2771
2772#[cfg(test)]
2773mod like_substring_tests {
2774 use super::{like_substring_match, like_substring_shape};
2775
2776 fn shape(p: &str) -> Option<(usize, alloc::string::String, usize)> {
2777 let chars: alloc::vec::Vec<char> = p.chars().collect();
2778 like_substring_shape(&chars)
2779 }
2780
2781 #[test]
2782 fn shape_recognition() {
2783 assert_eq!(shape("%_05%"), Some((1, "05".into(), 0)));
2784 assert_eq!(shape("%abc%"), Some((0, "abc".into(), 0)));
2785 assert_eq!(shape("%ab_%"), Some((0, "ab".into(), 1)));
2786 assert_eq!(shape("%%x%%"), Some((0, "x".into(), 0)));
2787 assert_eq!(shape("%__a__%"), Some((2, "a".into(), 2)));
2788 // Not eligible: missing anchors, inner %, escapes, empty literal.
2789 assert_eq!(shape("ab%"), None);
2790 assert_eq!(shape("%ab"), None);
2791 assert_eq!(shape("%a%b%"), None);
2792 assert_eq!(shape("%___%"), None);
2793 assert_eq!(shape("%a\\%b%"), None);
2794 assert_eq!(shape("%"), None);
2795 }
2796
2797 #[test]
2798 fn matcher_semantics() {
2799 // %_05% — needs one char before "05".
2800 assert!(like_substring_match("x05", "05", 1, 0));
2801 assert!(!like_substring_match("05", "05", 1, 0));
2802 assert!(like_substring_match("ab05cd", "05", 1, 0));
2803 // Overlapping / repeated hits: first hit fails the k-check,
2804 // a later one passes.
2805 assert!(like_substring_match("05x05", "05", 1, 0));
2806 // Trailing underscore needs one char after.
2807 assert!(like_substring_match("abz", "ab", 0, 1));
2808 assert!(!like_substring_match("ab", "ab", 0, 1));
2809 // Plain substring.
2810 assert!(like_substring_match("hello", "ell", 0, 0));
2811 assert!(!like_substring_match("hello", "xyz", 0, 0));
2812 // Multi-byte chars count as single wildcard chars.
2813 assert!(like_substring_match("é05", "05", 1, 0));
2814 assert!(!like_substring_match("é5", "05", 1, 0));
2815 }
2816}