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}