Skip to main content

oxigdal_algorithms/raster/calculator/
bytecode.rs

1//! Bytecode compiler and stack-machine VM for raster band-math expressions.
2//!
3//! # Overview
4//!
5//! The tree-walking [`Evaluator`] re-interprets the `Expr` AST for every pixel,
6//! which is expensive for million-pixel rasters.  This module provides a two-step
7//! alternative:
8//!
9//! 1. **Compile once**: [`compile_program`] traverses the AST exactly once and
10//!    emits a flat `Vec<OpCode>` together with a constant table.
11//! 2. **Execute per-pixel**: [`eval_bytecode`] interprets the opcode stream on a
12//!    tiny stack (`Vec<f64>`) that is pre-allocated outside the pixel loop.
13//!
14//! The resulting throughput improvement is typically 2–5× for real-world
15//! band-math expressions (NDVI, EVI, SAVI, …).
16//!
17//! # Band indexing
18//!
19//! Following the existing convention the AST node `Expr::Band(b)` is **1-indexed**
20//! (B1 ↔ index 1).  `LoadBand(i)` therefore stores the 1-based index unchanged
21//! and `eval_bytecode` subtracts one when indexing into `bands`.
22
23use super::ast::{BinaryOp, Expr, UnaryOp};
24use crate::error::{AlgorithmError, Result};
25use oxigdal_core::buffer::RasterBuffer;
26
27// ─────────────────────────────────────────────────────────────────────────────
28// OpCode
29// ─────────────────────────────────────────────────────────────────────────────
30
31/// A single instruction in the band-math VM.
32///
33/// The VM is a pure stack machine:  operands are pushed onto the stack and
34/// operations pop their arguments and push one result.
35#[derive(Debug, Clone, PartialEq)]
36pub enum OpCode {
37    // ── Stack load / store ────────────────────────────────────────────────
38    /// Push `constants[idx]` onto the stack.
39    LoadConst(u16),
40    /// Push `bands[idx - 1][pixel_idx]` (1-based band index).
41    LoadBand(u16),
42    /// Push the pre-computed CSE cache value at `cache[idx]`.
43    LoadCacheSlot(u16),
44    /// Pop the top of the stack and store it in `cache[idx]`.
45    StoreCacheSlot(u16),
46
47    // ── Arithmetic (binary: pop rhs, pop lhs, push result) ───────────────
48    /// Floating-point addition.
49    Add,
50    /// Floating-point subtraction.
51    Sub,
52    /// Floating-point multiplication.
53    Mul,
54    /// Floating-point division (returns NaN on divide-by-zero).
55    Div,
56    /// Power: lhs.powf(rhs).
57    Pow,
58
59    // ── Unary (pop one value, push result) ───────────────────────────────
60    /// Arithmetic negation.
61    Neg,
62    /// Absolute value.
63    Abs,
64    /// Square root.
65    Sqrt,
66    /// Log base 10 (`log10`).
67    Log,
68    /// Natural logarithm (`ln`).
69    Ln,
70    /// Exponential (`e^x`).
71    Exp,
72    /// Sine.
73    Sin,
74    /// Cosine.
75    Cos,
76    /// Tangent.
77    Tan,
78    /// Floor (round towards negative infinity).
79    Floor,
80    /// Ceiling (round towards positive infinity).
81    Ceil,
82    /// Round to nearest integer.
83    Round,
84
85    // ── Multi-argument (pops arguments right-to-left) ────────────────────
86    /// Pop b, pop a, push min(a, b).
87    Min,
88    /// Pop b, pop a, push max(a, b).
89    Max,
90    /// Pop max_v, pop min_v, pop val; push val.clamp(min_v, max_v).
91    Clamp,
92
93    // ── Comparisons (binary → 1.0 or 0.0) ────────────────────────────────
94    /// lhs > rhs  → 1.0 else 0.0
95    Gt,
96    /// lhs < rhs  → 1.0 else 0.0
97    Lt,
98    /// lhs >= rhs → 1.0 else 0.0
99    Gte,
100    /// lhs <= rhs → 1.0 else 0.0
101    Lte,
102    /// |lhs - rhs| < ε → 1.0 else 0.0
103    Eq,
104    /// |lhs - rhs| >= ε → 1.0 else 0.0
105    Ne,
106
107    // ── Logic (binary → 1.0 or 0.0) ──────────────────────────────────────
108    /// Both operands non-zero → 1.0 else 0.0
109    And,
110    /// At least one operand non-zero → 1.0 else 0.0
111    Or,
112
113    // ── Conditional ───────────────────────────────────────────────────────
114    /// Pop else_val (top), pop then_val, pop cond.
115    /// Push then_val if cond != 0.0, else else_val.
116    Cond,
117}
118
119// ─────────────────────────────────────────────────────────────────────────────
120// CompiledProgram
121// ─────────────────────────────────────────────────────────────────────────────
122
123/// A compiled band-math program, ready for repeated per-pixel execution.
124#[derive(Debug, Clone)]
125pub struct CompiledProgram {
126    /// The flat opcode stream.
127    pub ops: Vec<OpCode>,
128    /// Constant literals referenced by `LoadConst` instructions.
129    pub constants: Vec<f64>,
130    /// Number of input bands the program requires (0-based upper bound
131    /// derived from the highest `Band(b)` seen; 0 means no bands used).
132    pub required_bands: usize,
133    /// Number of CSE cache slots required.
134    pub cache_slot_count: usize,
135    /// Statically estimated maximum stack depth needed.
136    pub estimated_stack_depth: usize,
137}
138
139// ─────────────────────────────────────────────────────────────────────────────
140// compile_expr  (internal helper – populates a shared constant table)
141// ─────────────────────────────────────────────────────────────────────────────
142
143/// Compile a single `Expr` node into a sequence of `OpCode`s using post-order
144/// (operand-before-operator) emission.
145///
146/// `constants` is the shared literal table; existing constants are reused to
147/// keep `LoadConst` indices compact.
148///
149/// Note: this function is `pub(crate)` because `Expr` is private to the
150/// `calculator` module family; external callers must use
151/// [`RasterCalculator::evaluate_bytecode`] or [`compile_program`] via string
152/// parsing.
153///
154/// # Errors
155///
156/// Returns [`AlgorithmError::InvalidInput`] if the expression contains an
157/// unknown function name.
158pub(super) fn compile_expr(expr: &Expr, constants: &mut Vec<f64>) -> Result<Vec<OpCode>> {
159    let mut ops: Vec<OpCode> = Vec::new();
160    compile_expr_into(expr, constants, &mut ops)?;
161    Ok(ops)
162}
163
164/// Recursive helper that appends opcodes into `out` rather than allocating a
165/// new Vec for every sub-expression (avoids quadratic allocation).
166fn compile_expr_into(expr: &Expr, constants: &mut Vec<f64>, out: &mut Vec<OpCode>) -> Result<()> {
167    match expr {
168        // ── Leaf nodes ───────────────────────────────────────────────────
169        Expr::Number(v) => {
170            // Deduplicate constants by bit-level equality.
171            let idx = constants
172                .iter()
173                .position(|c| c.to_bits() == v.to_bits())
174                .unwrap_or_else(|| {
175                    let i = constants.len();
176                    constants.push(*v);
177                    i
178                });
179            let idx_u16 = u16::try_from(idx).map_err(|_| AlgorithmError::InvalidParameter {
180                parameter: "constants",
181                message: format!("Too many constants (max {})", u16::MAX),
182            })?;
183            out.push(OpCode::LoadConst(idx_u16));
184        }
185
186        Expr::Band(b) => {
187            let b_u16 = u16::try_from(*b).map_err(|_| AlgorithmError::InvalidParameter {
188                parameter: "band",
189                message: format!("Band index {} exceeds u16::MAX", b),
190            })?;
191            out.push(OpCode::LoadBand(b_u16));
192        }
193
194        Expr::CacheRef(idx) => {
195            let idx_u16 = u16::try_from(*idx).map_err(|_| AlgorithmError::InvalidParameter {
196                parameter: "cache_slot",
197                message: format!("Cache slot index {} exceeds u16::MAX", idx),
198            })?;
199            out.push(OpCode::LoadCacheSlot(idx_u16));
200        }
201
202        // ── Binary operations ─────────────────────────────────────────────
203        Expr::BinaryOp { left, op, right } => {
204            compile_expr_into(left, constants, out)?;
205            compile_expr_into(right, constants, out)?;
206            let opcode = match op {
207                BinaryOp::Add => OpCode::Add,
208                BinaryOp::Subtract => OpCode::Sub,
209                BinaryOp::Multiply => OpCode::Mul,
210                BinaryOp::Divide => OpCode::Div,
211                BinaryOp::Power => OpCode::Pow,
212                BinaryOp::Greater => OpCode::Gt,
213                BinaryOp::Less => OpCode::Lt,
214                BinaryOp::GreaterEqual => OpCode::Gte,
215                BinaryOp::LessEqual => OpCode::Lte,
216                BinaryOp::Equal => OpCode::Eq,
217                BinaryOp::NotEqual => OpCode::Ne,
218                BinaryOp::And => OpCode::And,
219                BinaryOp::Or => OpCode::Or,
220            };
221            out.push(opcode);
222        }
223
224        // ── Unary operations ──────────────────────────────────────────────
225        Expr::UnaryOp { op, expr: inner } => {
226            compile_expr_into(inner, constants, out)?;
227            let opcode = match op {
228                UnaryOp::Negate => OpCode::Neg,
229            };
230            out.push(opcode);
231        }
232
233        // ── Function calls ────────────────────────────────────────────────
234        Expr::Function { name, args } => {
235            compile_function(name, args, constants, out)?;
236        }
237
238        // ── Conditional (if/then/else) ────────────────────────────────────
239        Expr::Conditional {
240            condition,
241            then_expr,
242            else_expr,
243        } => {
244            // Stack layout: [... cond then_val else_val]  → Cond pops all 3.
245            compile_expr_into(condition, constants, out)?;
246            compile_expr_into(then_expr, constants, out)?;
247            compile_expr_into(else_expr, constants, out)?;
248            out.push(OpCode::Cond);
249        }
250    }
251    Ok(())
252}
253
254/// Compile a function call into the opcode stream.
255///
256/// Function arguments are compiled in order (left-to-right push).  Multi-arg
257/// opcodes (`Min`, `Max`, `Clamp`) pop their arguments in right-to-left order.
258fn compile_function(
259    name: &str,
260    args: &[Expr],
261    constants: &mut Vec<f64>,
262    out: &mut Vec<OpCode>,
263) -> Result<()> {
264    // Helper: verify exact argument count.
265    let check_arity = |expected: usize| -> Result<()> {
266        if args.len() != expected {
267            Err(AlgorithmError::InvalidInput(format!(
268                "Function '{}': expected {} argument(s), got {}",
269                name,
270                expected,
271                args.len()
272            )))
273        } else {
274            Ok(())
275        }
276    };
277
278    match name {
279        // ── 1-arg functions ───────────────────────────────────────────────
280        "sqrt" => {
281            check_arity(1)?;
282            compile_expr_into(&args[0], constants, out)?;
283            out.push(OpCode::Sqrt);
284        }
285        "abs" => {
286            check_arity(1)?;
287            compile_expr_into(&args[0], constants, out)?;
288            out.push(OpCode::Abs);
289        }
290        // "log" maps to natural logarithm (matching evaluator.rs behaviour)
291        "log" => {
292            check_arity(1)?;
293            compile_expr_into(&args[0], constants, out)?;
294            out.push(OpCode::Ln);
295        }
296        // "ln" explicit alias
297        "ln" => {
298            check_arity(1)?;
299            compile_expr_into(&args[0], constants, out)?;
300            out.push(OpCode::Ln);
301        }
302        // "log10" maps to base-10 logarithm
303        "log10" => {
304            check_arity(1)?;
305            compile_expr_into(&args[0], constants, out)?;
306            out.push(OpCode::Log);
307        }
308        "exp" => {
309            check_arity(1)?;
310            compile_expr_into(&args[0], constants, out)?;
311            out.push(OpCode::Exp);
312        }
313        "sin" => {
314            check_arity(1)?;
315            compile_expr_into(&args[0], constants, out)?;
316            out.push(OpCode::Sin);
317        }
318        "cos" => {
319            check_arity(1)?;
320            compile_expr_into(&args[0], constants, out)?;
321            out.push(OpCode::Cos);
322        }
323        "tan" => {
324            check_arity(1)?;
325            compile_expr_into(&args[0], constants, out)?;
326            out.push(OpCode::Tan);
327        }
328        "floor" => {
329            check_arity(1)?;
330            compile_expr_into(&args[0], constants, out)?;
331            out.push(OpCode::Floor);
332        }
333        "ceil" => {
334            check_arity(1)?;
335            compile_expr_into(&args[0], constants, out)?;
336            out.push(OpCode::Ceil);
337        }
338        "round" => {
339            check_arity(1)?;
340            compile_expr_into(&args[0], constants, out)?;
341            out.push(OpCode::Round);
342        }
343        // ── 2-arg functions ───────────────────────────────────────────────
344        "min" => {
345            check_arity(2)?;
346            compile_expr_into(&args[0], constants, out)?;
347            compile_expr_into(&args[1], constants, out)?;
348            out.push(OpCode::Min);
349        }
350        "max" => {
351            check_arity(2)?;
352            compile_expr_into(&args[0], constants, out)?;
353            compile_expr_into(&args[1], constants, out)?;
354            out.push(OpCode::Max);
355        }
356        // ── 3-arg functions ───────────────────────────────────────────────
357        "clamp" => {
358            check_arity(3)?;
359            compile_expr_into(&args[0], constants, out)?;
360            compile_expr_into(&args[1], constants, out)?;
361            compile_expr_into(&args[2], constants, out)?;
362            out.push(OpCode::Clamp);
363        }
364        // ── Unknown ───────────────────────────────────────────────────────
365        unknown => {
366            return Err(AlgorithmError::InvalidInput(format!(
367                "Unknown function: {unknown}"
368            )));
369        }
370    }
371    Ok(())
372}
373
374// ─────────────────────────────────────────────────────────────────────────────
375// compile_program
376// ─────────────────────────────────────────────────────────────────────────────
377
378/// Compile a full band-math program, optionally pre-computing CSE cache slots.
379///
380/// `cache_slots` should be the slice returned by the optimizer; each entry is
381/// the canonical sub-expression for that slot.  The emitted prelude evaluates
382/// each slot in order and stores it via `StoreCacheSlot(i)`.
383///
384/// Note: `pub(crate)` because `Expr` is private to the `calculator` module
385/// family.  External code must go through
386/// [`RasterCalculator::evaluate_bytecode`] which parses, optimises, and
387/// compiles internally.
388///
389/// # Errors
390///
391/// Propagates any [`AlgorithmError`] from [`compile_expr`].
392pub(super) fn compile_program(expr: &Expr, cache_slots: &[Expr]) -> Result<CompiledProgram> {
393    let mut constants: Vec<f64> = Vec::new();
394    let mut ops: Vec<OpCode> = Vec::new();
395
396    // Emit prelude: evaluate each CSE slot and store it.
397    for (i, slot_expr) in cache_slots.iter().enumerate() {
398        compile_expr_into(slot_expr, &mut constants, &mut ops)?;
399        let idx_u16 = u16::try_from(i).map_err(|_| AlgorithmError::InvalidParameter {
400            parameter: "cache_slots",
401            message: format!("Too many CSE cache slots (max {})", u16::MAX),
402        })?;
403        ops.push(OpCode::StoreCacheSlot(idx_u16));
404    }
405
406    // Emit the main expression.
407    compile_expr_into(expr, &mut constants, &mut ops)?;
408
409    // Determine the highest band index referenced.
410    let required_bands = highest_band_index(&ops);
411    let estimated_stack_depth = estimate_stack_depth(&ops);
412
413    Ok(CompiledProgram {
414        ops,
415        constants,
416        required_bands,
417        cache_slot_count: cache_slots.len(),
418        estimated_stack_depth,
419    })
420}
421
422/// Walk the opcode stream to find the maximum 1-based band index referenced.
423/// Returns 0 if no `LoadBand` instructions are present.
424fn highest_band_index(ops: &[OpCode]) -> usize {
425    ops.iter().fold(0usize, |acc, op| {
426        if let OpCode::LoadBand(b) = op {
427            acc.max(*b as usize)
428        } else {
429            acc
430        }
431    })
432}
433
434// ─────────────────────────────────────────────────────────────────────────────
435// estimate_stack_depth
436// ─────────────────────────────────────────────────────────────────────────────
437
438/// Statically estimate the maximum stack depth required to execute `ops`.
439///
440/// Each opcode is modelled as a pure delta on the stack pointer.  The function
441/// returns the maximum depth seen.
442pub fn estimate_stack_depth(ops: &[OpCode]) -> usize {
443    let mut depth: isize = 0;
444    let mut max_depth: isize = 0;
445
446    for op in ops {
447        let delta: isize = stack_delta(op);
448        depth += delta;
449        if depth > max_depth {
450            max_depth = depth;
451        }
452    }
453
454    max_depth.max(0) as usize
455}
456
457/// Returns the net stack depth change (+N = N pushes, -N = N pops net).
458fn stack_delta(op: &OpCode) -> isize {
459    match op {
460        // Push 1
461        OpCode::LoadConst(_) | OpCode::LoadBand(_) | OpCode::LoadCacheSlot(_) => 1,
462        // Pop 1, push 0 (net -1)
463        OpCode::StoreCacheSlot(_) => -1,
464        // Pop 2, push 1 (net -1)
465        OpCode::Add
466        | OpCode::Sub
467        | OpCode::Mul
468        | OpCode::Div
469        | OpCode::Pow
470        | OpCode::Gt
471        | OpCode::Lt
472        | OpCode::Gte
473        | OpCode::Lte
474        | OpCode::Eq
475        | OpCode::Ne
476        | OpCode::And
477        | OpCode::Or
478        | OpCode::Min
479        | OpCode::Max => -1,
480        // Pop 1, push 1 (net 0)
481        OpCode::Neg
482        | OpCode::Abs
483        | OpCode::Sqrt
484        | OpCode::Log
485        | OpCode::Ln
486        | OpCode::Exp
487        | OpCode::Sin
488        | OpCode::Cos
489        | OpCode::Tan
490        | OpCode::Floor
491        | OpCode::Ceil
492        | OpCode::Round => 0,
493        // Pop 3 (val, min, max), push 1 (net -2)
494        OpCode::Clamp => -2,
495        // Pop 3 (cond, then_val, else_val), push 1 (net -2)
496        OpCode::Cond => -2,
497    }
498}
499
500// ─────────────────────────────────────────────────────────────────────────────
501// eval_bytecode
502// ─────────────────────────────────────────────────────────────────────────────
503
504/// Execute the bytecode program for a single pixel.
505///
506/// # Arguments
507///
508/// * `prog`      – The compiled program (constant, shared across all pixels).
509/// * `bands`     – A slice of band data slices, 0-indexed (`bands[0]` = B1).
510/// * `pixel_idx` – Linear index into each band slice for the current pixel.
511/// * `cache`     – Pre-allocated `Vec<f64>` with `prog.cache_slot_count` entries.
512///                 Values are populated by `StoreCacheSlot` during the prelude.
513/// * `stack`     – Pre-allocated scratch `Vec<f64>`; cleared at the start of
514///                 each call.  Capacity should be `>= prog.estimated_stack_depth`.
515///
516/// # Errors
517///
518/// Returns [`AlgorithmError`] on stack underflow or band out-of-range.  Division
519/// by zero and invalid power operations return `f64::NAN` (not an error).
520pub fn eval_bytecode(
521    prog: &CompiledProgram,
522    bands: &[&[f64]],
523    pixel_idx: usize,
524    cache: &mut [f64],
525    stack: &mut Vec<f64>,
526) -> Result<f64> {
527    stack.clear();
528
529    for op in &prog.ops {
530        match op {
531            // ── Load / store ─────────────────────────────────────────────
532            OpCode::LoadConst(i) => {
533                let v = prog.constants.get(*i as usize).ok_or_else(|| {
534                    AlgorithmError::InvalidParameter {
535                        parameter: "LoadConst",
536                        message: format!(
537                            "Constant index {} out of range (len={})",
538                            i,
539                            prog.constants.len()
540                        ),
541                    }
542                })?;
543                stack.push(*v);
544            }
545
546            OpCode::LoadBand(b) => {
547                // b is 1-based.
548                let band_idx = (*b as usize).checked_sub(1).ok_or_else(|| {
549                    AlgorithmError::InvalidParameter {
550                        parameter: "LoadBand",
551                        message: "Band index 0 is invalid (bands are 1-indexed)".to_string(),
552                    }
553                })?;
554                let band_data =
555                    bands
556                        .get(band_idx)
557                        .ok_or_else(|| AlgorithmError::InvalidParameter {
558                            parameter: "LoadBand",
559                            message: format!(
560                                "Band {} out of range (have {} bands)",
561                                b,
562                                bands.len()
563                            ),
564                        })?;
565                let v =
566                    band_data
567                        .get(pixel_idx)
568                        .ok_or_else(|| AlgorithmError::InvalidParameter {
569                            parameter: "LoadBand",
570                            message: format!(
571                                "Pixel index {} out of range for band {}",
572                                pixel_idx, b
573                            ),
574                        })?;
575                stack.push(*v);
576            }
577
578            OpCode::LoadCacheSlot(i) => {
579                let v = cache
580                    .get(*i as usize)
581                    .ok_or_else(|| AlgorithmError::InvalidParameter {
582                        parameter: "LoadCacheSlot",
583                        message: format!("Cache slot {} out of range (len={})", i, cache.len()),
584                    })?;
585                stack.push(*v);
586            }
587
588            OpCode::StoreCacheSlot(i) => {
589                let v = stack_pop(stack)?;
590                let slot_idx = *i as usize;
591                let cache_len = cache.len();
592                let slot =
593                    cache
594                        .get_mut(slot_idx)
595                        .ok_or_else(|| AlgorithmError::InvalidParameter {
596                            parameter: "StoreCacheSlot",
597                            message: format!(
598                                "Cache slot {} out of range (len={})",
599                                slot_idx, cache_len
600                            ),
601                        })?;
602                *slot = v;
603            }
604
605            // ── Binary arithmetic ─────────────────────────────────────────
606            OpCode::Add => {
607                let rhs = stack_pop(stack)?;
608                let lhs = stack_pop(stack)?;
609                stack.push(lhs + rhs);
610            }
611            OpCode::Sub => {
612                let rhs = stack_pop(stack)?;
613                let lhs = stack_pop(stack)?;
614                stack.push(lhs - rhs);
615            }
616            OpCode::Mul => {
617                let rhs = stack_pop(stack)?;
618                let lhs = stack_pop(stack)?;
619                stack.push(lhs * rhs);
620            }
621            OpCode::Div => {
622                let rhs = stack_pop(stack)?;
623                let lhs = stack_pop(stack)?;
624                // Division by zero → NaN (matching tree-walk evaluator).
625                if rhs.abs() < f64::EPSILON {
626                    stack.push(f64::NAN);
627                } else {
628                    stack.push(lhs / rhs);
629                }
630            }
631            OpCode::Pow => {
632                let rhs = stack_pop(stack)?;
633                let lhs = stack_pop(stack)?;
634                stack.push(lhs.powf(rhs));
635            }
636
637            // ── Unary ─────────────────────────────────────────────────────
638            OpCode::Neg => {
639                let v = stack_pop(stack)?;
640                stack.push(-v);
641            }
642            OpCode::Abs => {
643                let v = stack_pop(stack)?;
644                stack.push(v.abs());
645            }
646            OpCode::Sqrt => {
647                let v = stack_pop(stack)?;
648                stack.push(v.sqrt());
649            }
650            OpCode::Log => {
651                let v = stack_pop(stack)?;
652                stack.push(v.log10());
653            }
654            OpCode::Ln => {
655                let v = stack_pop(stack)?;
656                stack.push(v.ln());
657            }
658            OpCode::Exp => {
659                let v = stack_pop(stack)?;
660                stack.push(v.exp());
661            }
662            OpCode::Sin => {
663                let v = stack_pop(stack)?;
664                stack.push(v.sin());
665            }
666            OpCode::Cos => {
667                let v = stack_pop(stack)?;
668                stack.push(v.cos());
669            }
670            OpCode::Tan => {
671                let v = stack_pop(stack)?;
672                stack.push(v.tan());
673            }
674            OpCode::Floor => {
675                let v = stack_pop(stack)?;
676                stack.push(v.floor());
677            }
678            OpCode::Ceil => {
679                let v = stack_pop(stack)?;
680                stack.push(v.ceil());
681            }
682            OpCode::Round => {
683                let v = stack_pop(stack)?;
684                stack.push(v.round());
685            }
686
687            // ── Multi-arg ─────────────────────────────────────────────────
688            OpCode::Min => {
689                let b = stack_pop(stack)?;
690                let a = stack_pop(stack)?;
691                stack.push(a.min(b));
692            }
693            OpCode::Max => {
694                let b = stack_pop(stack)?;
695                let a = stack_pop(stack)?;
696                stack.push(a.max(b));
697            }
698            OpCode::Clamp => {
699                let max_v = stack_pop(stack)?;
700                let min_v = stack_pop(stack)?;
701                let val = stack_pop(stack)?;
702                stack.push(val.clamp(min_v, max_v));
703            }
704
705            // ── Comparisons ───────────────────────────────────────────────
706            OpCode::Gt => {
707                let rhs = stack_pop(stack)?;
708                let lhs = stack_pop(stack)?;
709                stack.push(if lhs > rhs { 1.0 } else { 0.0 });
710            }
711            OpCode::Lt => {
712                let rhs = stack_pop(stack)?;
713                let lhs = stack_pop(stack)?;
714                stack.push(if lhs < rhs { 1.0 } else { 0.0 });
715            }
716            OpCode::Gte => {
717                let rhs = stack_pop(stack)?;
718                let lhs = stack_pop(stack)?;
719                stack.push(if lhs >= rhs { 1.0 } else { 0.0 });
720            }
721            OpCode::Lte => {
722                let rhs = stack_pop(stack)?;
723                let lhs = stack_pop(stack)?;
724                stack.push(if lhs <= rhs { 1.0 } else { 0.0 });
725            }
726            OpCode::Eq => {
727                let rhs = stack_pop(stack)?;
728                let lhs = stack_pop(stack)?;
729                stack.push(if (lhs - rhs).abs() < f64::EPSILON {
730                    1.0
731                } else {
732                    0.0
733                });
734            }
735            OpCode::Ne => {
736                let rhs = stack_pop(stack)?;
737                let lhs = stack_pop(stack)?;
738                stack.push(if (lhs - rhs).abs() >= f64::EPSILON {
739                    1.0
740                } else {
741                    0.0
742                });
743            }
744
745            // ── Logic ─────────────────────────────────────────────────────
746            OpCode::And => {
747                let rhs = stack_pop(stack)?;
748                let lhs = stack_pop(stack)?;
749                stack.push(if lhs != 0.0 && rhs != 0.0 { 1.0 } else { 0.0 });
750            }
751            OpCode::Or => {
752                let rhs = stack_pop(stack)?;
753                let lhs = stack_pop(stack)?;
754                stack.push(if lhs != 0.0 || rhs != 0.0 { 1.0 } else { 0.0 });
755            }
756
757            // ── Conditional ───────────────────────────────────────────────
758            OpCode::Cond => {
759                // Stack order (top-of-stack last):  [..., cond, then_val, else_val]
760                let else_val = stack_pop(stack)?;
761                let then_val = stack_pop(stack)?;
762                let cond = stack_pop(stack)?;
763                stack.push(if cond != 0.0 { then_val } else { else_val });
764            }
765        }
766    }
767
768    // After execution the stack must contain exactly one value.
769    if stack.len() != 1 {
770        return Err(AlgorithmError::ComputationError(format!(
771            "eval_bytecode: expected stack depth 1 after execution, got {}",
772            stack.len()
773        )));
774    }
775    stack_pop(stack)
776}
777
778/// Pop the top of `stack`, returning a stack-underflow error on empty stack.
779#[inline]
780fn stack_pop(stack: &mut Vec<f64>) -> Result<f64> {
781    stack.pop().ok_or_else(|| {
782        AlgorithmError::ComputationError("eval_bytecode: stack underflow".to_string())
783    })
784}
785
786// ─────────────────────────────────────────────────────────────────────────────
787// RasterCalculator::evaluate_bytecode  (impl extension)
788// ─────────────────────────────────────────────────────────────────────────────
789
790use super::ops::RasterCalculator;
791use super::{lexer::Lexer, optimizer::Optimizer, parser::Parser};
792
793impl RasterCalculator {
794    /// Evaluate a band-math expression using the bytecode VM.
795    ///
796    /// Parsing and compilation happen once; the resulting [`CompiledProgram`] is
797    /// then executed for every pixel with a pair of pre-allocated scratch buffers
798    /// (`cache` and `stack`) so no heap allocations occur inside the pixel loop.
799    ///
800    /// This method is functionally equivalent to [`RasterCalculator::evaluate`]
801    /// but can be 2–5× faster on large rasters because it avoids AST tree
802    /// traversal per pixel.
803    ///
804    /// # Arguments
805    ///
806    /// * `expr_str` – The expression string (e.g. `"(B1 - B2) / (B1 + B2)"`).
807    /// * `bands`    – Input `RasterBuffer` slices; B1 = bands\[0\], B2 = bands\[1\], …
808    ///
809    /// # Errors
810    ///
811    /// Returns an [`AlgorithmError`] if:
812    /// - `bands` is empty ([`AlgorithmError::EmptyInput`]),
813    /// - band dimensions differ ([`AlgorithmError::InvalidDimensions`]),
814    /// - the expression cannot be parsed or compiled.
815    pub fn evaluate_bytecode(expr_str: &str, bands: &[RasterBuffer]) -> Result<RasterBuffer> {
816        if bands.is_empty() {
817            return Err(AlgorithmError::EmptyInput {
818                operation: "evaluate_bytecode",
819            });
820        }
821
822        // Validate that all bands share the same dimensions.
823        let width = bands[0].width();
824        let height = bands[0].height();
825        for band in bands.iter().skip(1) {
826            if band.width() != width || band.height() != height {
827                return Err(AlgorithmError::InvalidDimensions {
828                    message: "All bands must have same dimensions",
829                    actual: band.width() as usize,
830                    expected: width as usize,
831                });
832            }
833        }
834
835        // ── Parse ────────────────────────────────────────────────────────
836        let mut lexer = Lexer::new(expr_str);
837        let tokens = lexer.tokenize()?;
838        let mut parser = Parser::new(tokens);
839        let raw_expr = parser.parse()?;
840
841        // ── Optimize (CSE) ───────────────────────────────────────────────
842        let (expr, cache_slots) = Optimizer::optimize(raw_expr);
843
844        // ── Compile ──────────────────────────────────────────────────────
845        let prog = compile_program(&expr, &cache_slots)?;
846
847        // ── Extract flat pixel data from each RasterBuffer ───────────────
848        // We collect per-band pixel slices once (avoids repeated get_pixel calls).
849        let num_pixels = (width as usize) * (height as usize);
850        let band_data: Vec<Vec<f64>> = bands
851            .iter()
852            .map(|b| collect_band_pixels(b, width, height))
853            .collect();
854        let band_slices: Vec<&[f64]> = band_data.iter().map(|v| v.as_slice()).collect();
855
856        // Validate that the expression doesn't reference more bands than supplied.
857        if prog.required_bands > bands.len() {
858            return Err(AlgorithmError::InvalidParameter {
859                parameter: "band",
860                message: format!(
861                    "Expression references band {} but only {} band(s) provided",
862                    prog.required_bands,
863                    bands.len()
864                ),
865            });
866        }
867
868        // ── Pre-allocate scratch buffers (once for the whole raster) ─────
869        let mut cache = vec![0.0f64; prog.cache_slot_count];
870        let mut vm_stack: Vec<f64> = Vec::with_capacity(prog.estimated_stack_depth.max(8));
871
872        let mut result = RasterBuffer::zeros(width, height, bands[0].data_type());
873
874        for pixel_idx in 0..num_pixels {
875            // Reset the CSE cache for each pixel (each pixel is independent).
876            for slot in cache.iter_mut() {
877                *slot = 0.0;
878            }
879
880            let value = eval_bytecode(&prog, &band_slices, pixel_idx, &mut cache, &mut vm_stack)?;
881
882            let x = (pixel_idx % width as usize) as u64;
883            let y = (pixel_idx / width as usize) as u64;
884            result
885                .set_pixel(x, y, value)
886                .map_err(AlgorithmError::Core)?;
887        }
888
889        Ok(result)
890    }
891}
892
893/// Collect all pixels from a `RasterBuffer` into a flat `Vec<f64>` in
894/// row-major order (y-outer, x-inner).
895fn collect_band_pixels(band: &RasterBuffer, width: u64, height: u64) -> Vec<f64> {
896    let mut data = Vec::with_capacity((width * height) as usize);
897    for y in 0..height {
898        for x in 0..width {
899            // Use 0.0 as a fallback for out-of-bounds (shouldn't happen after
900            // dimension validation, but we must not panic).
901            let v = band.get_pixel(x, y).unwrap_or(0.0);
902            data.push(v);
903        }
904    }
905    data
906}
907
908// ─────────────────────────────────────────────────────────────────────────────
909// Unit tests (inline — require access to private Expr/BinaryOp/UnaryOp types)
910// ─────────────────────────────────────────────────────────────────────────────
911
912#[cfg(test)]
913#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
914mod tests {
915    use super::*;
916    use crate::raster::calculator::ast::{BinaryOp, Expr, UnaryOp};
917
918    // ── Test 1: Number literal emits LoadConst ───────────────────────────────
919    #[test]
920    fn test_compile_number_emits_load_const() {
921        // Use an arbitrary non-special literal to avoid approx_constant lint.
922        let val = 12.5_f64;
923        let expr = Expr::Number(val);
924        let mut constants = Vec::new();
925        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
926        assert_eq!(ops.len(), 1, "expected exactly one opcode");
927        assert_eq!(
928            ops[0],
929            OpCode::LoadConst(0),
930            "first opcode must be LoadConst(0)"
931        );
932        assert!(
933            (constants[0] - val).abs() < f64::EPSILON,
934            "constant[0] must equal the compiled value"
935        );
936    }
937
938    // ── Test 2: Band reference emits LoadBand ────────────────────────────────
939    #[test]
940    fn test_compile_band_emits_load_band() {
941        let expr = Expr::Band(0);
942        let mut constants = Vec::new();
943        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
944        assert_eq!(ops.len(), 1);
945        assert_eq!(ops[0], OpCode::LoadBand(0));
946    }
947
948    // ── Test 3: Binary add emits two LoadConst + Add ─────────────────────────
949    #[test]
950    fn test_compile_add_binary() {
951        let expr = Expr::BinaryOp {
952            left: Box::new(Expr::Number(1.0)),
953            op: BinaryOp::Add,
954            right: Box::new(Expr::Number(2.0)),
955        };
956        let mut constants = Vec::new();
957        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
958        assert_eq!(ops.len(), 3, "expected [LoadConst, LoadConst, Add]");
959        assert!(matches!(ops[0], OpCode::LoadConst(_)));
960        assert!(matches!(ops[1], OpCode::LoadConst(_)));
961        assert_eq!(ops[2], OpCode::Add);
962    }
963
964    // ── Test 4: Function sqrt emits LoadBand + Sqrt ──────────────────────────
965    #[test]
966    fn test_compile_function_sqrt() {
967        let expr = Expr::Function {
968            name: "sqrt".to_string(),
969            args: vec![Expr::Band(0)],
970        };
971        let mut constants = Vec::new();
972        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
973        assert_eq!(ops.len(), 2, "expected [LoadBand(0), Sqrt]");
974        assert_eq!(ops[0], OpCode::LoadBand(0));
975        assert_eq!(ops[1], OpCode::Sqrt);
976    }
977
978    // ── Test 5: Conditional expression ends with Cond ────────────────────────
979    #[test]
980    fn test_compile_conditional() {
981        let expr = Expr::Conditional {
982            condition: Box::new(Expr::Number(1.0)),
983            then_expr: Box::new(Expr::Number(2.0)),
984            else_expr: Box::new(Expr::Number(3.0)),
985        };
986        let mut constants = Vec::new();
987        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
988        // Expected: [LoadConst, LoadConst, LoadConst, Cond]
989        assert!(ops.len() >= 2, "expected at least 2 opcodes");
990        assert_eq!(*ops.last().expect("ops must not be empty"), OpCode::Cond);
991    }
992
993    // ── Test 13: estimate_stack_depth for simple add ──────────────────────────
994    #[test]
995    fn test_estimate_stack_depth_simple_add() {
996        // (1.0 + 2.0) compiles to [LoadConst(0), LoadConst(1), Add]
997        // Depth trace: 0 → 1 → 2 → 1  (max = 2)
998        let expr = Expr::BinaryOp {
999            left: Box::new(Expr::Number(1.0)),
1000            op: BinaryOp::Add,
1001            right: Box::new(Expr::Number(2.0)),
1002        };
1003        let mut constants = Vec::new();
1004        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
1005        let depth = estimate_stack_depth(&ops);
1006        assert_eq!(depth, 2, "simple add needs stack depth 2");
1007    }
1008
1009    // ── Test: unary negate compiles correctly ────────────────────────────────
1010    #[test]
1011    fn test_compile_unary_negate() {
1012        let expr = Expr::UnaryOp {
1013            op: UnaryOp::Negate,
1014            expr: Box::new(Expr::Band(1)),
1015        };
1016        let mut constants = Vec::new();
1017        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
1018        assert_eq!(ops.len(), 2);
1019        assert_eq!(ops[0], OpCode::LoadBand(1));
1020        assert_eq!(ops[1], OpCode::Neg);
1021    }
1022
1023    // ── Test: constant deduplication ─────────────────────────────────────────
1024    #[test]
1025    fn test_constant_deduplication() {
1026        // Two references to 1.0 should share the same constants[0] index.
1027        let expr = Expr::BinaryOp {
1028            left: Box::new(Expr::Number(1.0)),
1029            op: BinaryOp::Add,
1030            right: Box::new(Expr::Number(1.0)),
1031        };
1032        let mut constants = Vec::new();
1033        let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
1034        assert_eq!(constants.len(), 1, "1.0 should be stored once");
1035        assert!(matches!(ops[0], OpCode::LoadConst(0)));
1036        assert!(matches!(ops[1], OpCode::LoadConst(0)));
1037        assert_eq!(ops[2], OpCode::Add);
1038    }
1039
1040    // ── Test: eval_bytecode with a single constant ────────────────────────────
1041    #[test]
1042    fn test_eval_bytecode_constant_inline() {
1043        let prog = CompiledProgram {
1044            ops: vec![OpCode::LoadConst(0)],
1045            constants: vec![42.0],
1046            required_bands: 0,
1047            cache_slot_count: 0,
1048            estimated_stack_depth: 1,
1049        };
1050        let mut cache = Vec::new();
1051        let mut stack = Vec::with_capacity(4);
1052        let result =
1053            eval_bytecode(&prog, &[], 0, &mut cache, &mut stack).expect("eval should succeed");
1054        assert!((result - 42.0).abs() < f64::EPSILON);
1055    }
1056}