Skip to main content

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