use super::ast::{BinaryOp, Expr, UnaryOp};
use crate::error::{AlgorithmError, Result};
use oxigdal_core::buffer::RasterBuffer;
#[derive(Debug, Clone, PartialEq)]
pub enum OpCode {
LoadConst(u16),
LoadBand(u16),
LoadCacheSlot(u16),
StoreCacheSlot(u16),
Add,
Sub,
Mul,
Div,
Pow,
Neg,
Abs,
Sqrt,
Log,
Ln,
Exp,
Sin,
Cos,
Tan,
Floor,
Ceil,
Round,
Min,
Max,
Clamp,
Gt,
Lt,
Gte,
Lte,
Eq,
Ne,
And,
Or,
Cond,
}
#[derive(Debug, Clone)]
pub struct CompiledProgram {
pub ops: Vec<OpCode>,
pub constants: Vec<f64>,
pub required_bands: usize,
pub cache_slot_count: usize,
pub estimated_stack_depth: usize,
}
pub(super) fn compile_expr(expr: &Expr, constants: &mut Vec<f64>) -> Result<Vec<OpCode>> {
let mut ops: Vec<OpCode> = Vec::new();
compile_expr_into(expr, constants, &mut ops)?;
Ok(ops)
}
fn compile_expr_into(expr: &Expr, constants: &mut Vec<f64>, out: &mut Vec<OpCode>) -> Result<()> {
match expr {
Expr::Number(v) => {
let idx = constants
.iter()
.position(|c| c.to_bits() == v.to_bits())
.unwrap_or_else(|| {
let i = constants.len();
constants.push(*v);
i
});
let idx_u16 = u16::try_from(idx).map_err(|_| AlgorithmError::InvalidParameter {
parameter: "constants",
message: format!("Too many constants (max {})", u16::MAX),
})?;
out.push(OpCode::LoadConst(idx_u16));
}
Expr::Band(b) => {
let b_u16 = u16::try_from(*b).map_err(|_| AlgorithmError::InvalidParameter {
parameter: "band",
message: format!("Band index {} exceeds u16::MAX", b),
})?;
out.push(OpCode::LoadBand(b_u16));
}
Expr::CacheRef(idx) => {
let idx_u16 = u16::try_from(*idx).map_err(|_| AlgorithmError::InvalidParameter {
parameter: "cache_slot",
message: format!("Cache slot index {} exceeds u16::MAX", idx),
})?;
out.push(OpCode::LoadCacheSlot(idx_u16));
}
Expr::BinaryOp { left, op, right } => {
compile_expr_into(left, constants, out)?;
compile_expr_into(right, constants, out)?;
let opcode = match op {
BinaryOp::Add => OpCode::Add,
BinaryOp::Subtract => OpCode::Sub,
BinaryOp::Multiply => OpCode::Mul,
BinaryOp::Divide => OpCode::Div,
BinaryOp::Power => OpCode::Pow,
BinaryOp::Greater => OpCode::Gt,
BinaryOp::Less => OpCode::Lt,
BinaryOp::GreaterEqual => OpCode::Gte,
BinaryOp::LessEqual => OpCode::Lte,
BinaryOp::Equal => OpCode::Eq,
BinaryOp::NotEqual => OpCode::Ne,
BinaryOp::And => OpCode::And,
BinaryOp::Or => OpCode::Or,
};
out.push(opcode);
}
Expr::UnaryOp { op, expr: inner } => {
compile_expr_into(inner, constants, out)?;
let opcode = match op {
UnaryOp::Negate => OpCode::Neg,
};
out.push(opcode);
}
Expr::Function { name, args } => {
compile_function(name, args, constants, out)?;
}
Expr::Conditional {
condition,
then_expr,
else_expr,
} => {
compile_expr_into(condition, constants, out)?;
compile_expr_into(then_expr, constants, out)?;
compile_expr_into(else_expr, constants, out)?;
out.push(OpCode::Cond);
}
}
Ok(())
}
fn compile_function(
name: &str,
args: &[Expr],
constants: &mut Vec<f64>,
out: &mut Vec<OpCode>,
) -> Result<()> {
let check_arity = |expected: usize| -> Result<()> {
if args.len() != expected {
Err(AlgorithmError::InvalidInput(format!(
"Function '{}': expected {} argument(s), got {}",
name,
expected,
args.len()
)))
} else {
Ok(())
}
};
match name {
"sqrt" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Sqrt);
}
"abs" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Abs);
}
"log" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Ln);
}
"ln" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Ln);
}
"log10" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Log);
}
"exp" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Exp);
}
"sin" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Sin);
}
"cos" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Cos);
}
"tan" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Tan);
}
"floor" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Floor);
}
"ceil" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Ceil);
}
"round" => {
check_arity(1)?;
compile_expr_into(&args[0], constants, out)?;
out.push(OpCode::Round);
}
"min" => {
check_arity(2)?;
compile_expr_into(&args[0], constants, out)?;
compile_expr_into(&args[1], constants, out)?;
out.push(OpCode::Min);
}
"max" => {
check_arity(2)?;
compile_expr_into(&args[0], constants, out)?;
compile_expr_into(&args[1], constants, out)?;
out.push(OpCode::Max);
}
"clamp" => {
check_arity(3)?;
compile_expr_into(&args[0], constants, out)?;
compile_expr_into(&args[1], constants, out)?;
compile_expr_into(&args[2], constants, out)?;
out.push(OpCode::Clamp);
}
unknown => {
return Err(AlgorithmError::InvalidInput(format!(
"Unknown function: {unknown}"
)));
}
}
Ok(())
}
pub(super) fn compile_program(expr: &Expr, cache_slots: &[Expr]) -> Result<CompiledProgram> {
let mut constants: Vec<f64> = Vec::new();
let mut ops: Vec<OpCode> = Vec::new();
for (i, slot_expr) in cache_slots.iter().enumerate() {
compile_expr_into(slot_expr, &mut constants, &mut ops)?;
let idx_u16 = u16::try_from(i).map_err(|_| AlgorithmError::InvalidParameter {
parameter: "cache_slots",
message: format!("Too many CSE cache slots (max {})", u16::MAX),
})?;
ops.push(OpCode::StoreCacheSlot(idx_u16));
}
compile_expr_into(expr, &mut constants, &mut ops)?;
let required_bands = highest_band_index(&ops);
let estimated_stack_depth = estimate_stack_depth(&ops);
Ok(CompiledProgram {
ops,
constants,
required_bands,
cache_slot_count: cache_slots.len(),
estimated_stack_depth,
})
}
fn highest_band_index(ops: &[OpCode]) -> usize {
ops.iter().fold(0usize, |acc, op| {
if let OpCode::LoadBand(b) = op {
acc.max(*b as usize)
} else {
acc
}
})
}
pub fn estimate_stack_depth(ops: &[OpCode]) -> usize {
let mut depth: isize = 0;
let mut max_depth: isize = 0;
for op in ops {
let delta: isize = stack_delta(op);
depth += delta;
if depth > max_depth {
max_depth = depth;
}
}
max_depth.max(0) as usize
}
fn stack_delta(op: &OpCode) -> isize {
match op {
OpCode::LoadConst(_) | OpCode::LoadBand(_) | OpCode::LoadCacheSlot(_) => 1,
OpCode::StoreCacheSlot(_) => -1,
OpCode::Add
| OpCode::Sub
| OpCode::Mul
| OpCode::Div
| OpCode::Pow
| OpCode::Gt
| OpCode::Lt
| OpCode::Gte
| OpCode::Lte
| OpCode::Eq
| OpCode::Ne
| OpCode::And
| OpCode::Or
| OpCode::Min
| OpCode::Max => -1,
OpCode::Neg
| OpCode::Abs
| OpCode::Sqrt
| OpCode::Log
| OpCode::Ln
| OpCode::Exp
| OpCode::Sin
| OpCode::Cos
| OpCode::Tan
| OpCode::Floor
| OpCode::Ceil
| OpCode::Round => 0,
OpCode::Clamp => -2,
OpCode::Cond => -2,
}
}
pub fn eval_bytecode(
prog: &CompiledProgram,
bands: &[&[f64]],
pixel_idx: usize,
cache: &mut [f64],
stack: &mut Vec<f64>,
) -> Result<f64> {
stack.clear();
for op in &prog.ops {
match op {
OpCode::LoadConst(i) => {
let v = prog.constants.get(*i as usize).ok_or_else(|| {
AlgorithmError::InvalidParameter {
parameter: "LoadConst",
message: format!(
"Constant index {} out of range (len={})",
i,
prog.constants.len()
),
}
})?;
stack.push(*v);
}
OpCode::LoadBand(b) => {
let band_idx = (*b as usize).checked_sub(1).ok_or_else(|| {
AlgorithmError::InvalidParameter {
parameter: "LoadBand",
message: "Band index 0 is invalid (bands are 1-indexed)".to_string(),
}
})?;
let band_data =
bands
.get(band_idx)
.ok_or_else(|| AlgorithmError::InvalidParameter {
parameter: "LoadBand",
message: format!(
"Band {} out of range (have {} bands)",
b,
bands.len()
),
})?;
let v =
band_data
.get(pixel_idx)
.ok_or_else(|| AlgorithmError::InvalidParameter {
parameter: "LoadBand",
message: format!(
"Pixel index {} out of range for band {}",
pixel_idx, b
),
})?;
stack.push(*v);
}
OpCode::LoadCacheSlot(i) => {
let v = cache
.get(*i as usize)
.ok_or_else(|| AlgorithmError::InvalidParameter {
parameter: "LoadCacheSlot",
message: format!("Cache slot {} out of range (len={})", i, cache.len()),
})?;
stack.push(*v);
}
OpCode::StoreCacheSlot(i) => {
let v = stack_pop(stack)?;
let slot_idx = *i as usize;
let cache_len = cache.len();
let slot =
cache
.get_mut(slot_idx)
.ok_or_else(|| AlgorithmError::InvalidParameter {
parameter: "StoreCacheSlot",
message: format!(
"Cache slot {} out of range (len={})",
slot_idx, cache_len
),
})?;
*slot = v;
}
OpCode::Add => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(lhs + rhs);
}
OpCode::Sub => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(lhs - rhs);
}
OpCode::Mul => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(lhs * rhs);
}
OpCode::Div => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
if rhs.abs() < f64::EPSILON {
stack.push(f64::NAN);
} else {
stack.push(lhs / rhs);
}
}
OpCode::Pow => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(lhs.powf(rhs));
}
OpCode::Neg => {
let v = stack_pop(stack)?;
stack.push(-v);
}
OpCode::Abs => {
let v = stack_pop(stack)?;
stack.push(v.abs());
}
OpCode::Sqrt => {
let v = stack_pop(stack)?;
stack.push(v.sqrt());
}
OpCode::Log => {
let v = stack_pop(stack)?;
stack.push(v.log10());
}
OpCode::Ln => {
let v = stack_pop(stack)?;
stack.push(v.ln());
}
OpCode::Exp => {
let v = stack_pop(stack)?;
stack.push(v.exp());
}
OpCode::Sin => {
let v = stack_pop(stack)?;
stack.push(v.sin());
}
OpCode::Cos => {
let v = stack_pop(stack)?;
stack.push(v.cos());
}
OpCode::Tan => {
let v = stack_pop(stack)?;
stack.push(v.tan());
}
OpCode::Floor => {
let v = stack_pop(stack)?;
stack.push(v.floor());
}
OpCode::Ceil => {
let v = stack_pop(stack)?;
stack.push(v.ceil());
}
OpCode::Round => {
let v = stack_pop(stack)?;
stack.push(v.round());
}
OpCode::Min => {
let b = stack_pop(stack)?;
let a = stack_pop(stack)?;
stack.push(a.min(b));
}
OpCode::Max => {
let b = stack_pop(stack)?;
let a = stack_pop(stack)?;
stack.push(a.max(b));
}
OpCode::Clamp => {
let max_v = stack_pop(stack)?;
let min_v = stack_pop(stack)?;
let val = stack_pop(stack)?;
stack.push(val.clamp(min_v, max_v));
}
OpCode::Gt => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs > rhs { 1.0 } else { 0.0 });
}
OpCode::Lt => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs < rhs { 1.0 } else { 0.0 });
}
OpCode::Gte => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs >= rhs { 1.0 } else { 0.0 });
}
OpCode::Lte => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs <= rhs { 1.0 } else { 0.0 });
}
OpCode::Eq => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if (lhs - rhs).abs() < f64::EPSILON {
1.0
} else {
0.0
});
}
OpCode::Ne => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if (lhs - rhs).abs() >= f64::EPSILON {
1.0
} else {
0.0
});
}
OpCode::And => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs != 0.0 && rhs != 0.0 { 1.0 } else { 0.0 });
}
OpCode::Or => {
let rhs = stack_pop(stack)?;
let lhs = stack_pop(stack)?;
stack.push(if lhs != 0.0 || rhs != 0.0 { 1.0 } else { 0.0 });
}
OpCode::Cond => {
let else_val = stack_pop(stack)?;
let then_val = stack_pop(stack)?;
let cond = stack_pop(stack)?;
stack.push(if cond != 0.0 { then_val } else { else_val });
}
}
}
if stack.len() != 1 {
return Err(AlgorithmError::ComputationError(format!(
"eval_bytecode: expected stack depth 1 after execution, got {}",
stack.len()
)));
}
stack_pop(stack)
}
#[inline]
fn stack_pop(stack: &mut Vec<f64>) -> Result<f64> {
stack.pop().ok_or_else(|| {
AlgorithmError::ComputationError("eval_bytecode: stack underflow".to_string())
})
}
use super::ops::RasterCalculator;
use super::{lexer::Lexer, optimizer::Optimizer, parser::Parser};
impl RasterCalculator {
pub fn evaluate_bytecode(expr_str: &str, bands: &[RasterBuffer]) -> Result<RasterBuffer> {
if bands.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "evaluate_bytecode",
});
}
let width = bands[0].width();
let height = bands[0].height();
for band in bands.iter().skip(1) {
if band.width() != width || band.height() != height {
return Err(AlgorithmError::InvalidDimensions {
message: "All bands must have same dimensions",
actual: band.width() as usize,
expected: width as usize,
});
}
}
let mut lexer = Lexer::new(expr_str);
let tokens = lexer.tokenize()?;
let mut parser = Parser::new(tokens);
let raw_expr = parser.parse()?;
let (expr, cache_slots) = Optimizer::optimize(raw_expr);
let prog = compile_program(&expr, &cache_slots)?;
let num_pixels = (width as usize) * (height as usize);
let band_data: Vec<Vec<f64>> = bands
.iter()
.map(|b| collect_band_pixels(b, width, height))
.collect();
let band_slices: Vec<&[f64]> = band_data.iter().map(|v| v.as_slice()).collect();
if prog.required_bands > bands.len() {
return Err(AlgorithmError::InvalidParameter {
parameter: "band",
message: format!(
"Expression references band {} but only {} band(s) provided",
prog.required_bands,
bands.len()
),
});
}
let mut cache = vec![0.0f64; prog.cache_slot_count];
let mut vm_stack: Vec<f64> = Vec::with_capacity(prog.estimated_stack_depth.max(8));
let mut result = RasterBuffer::zeros(width, height, bands[0].data_type());
for pixel_idx in 0..num_pixels {
for slot in cache.iter_mut() {
*slot = 0.0;
}
let value = eval_bytecode(&prog, &band_slices, pixel_idx, &mut cache, &mut vm_stack)?;
let x = (pixel_idx % width as usize) as u64;
let y = (pixel_idx / width as usize) as u64;
result
.set_pixel(x, y, value)
.map_err(AlgorithmError::Core)?;
}
Ok(result)
}
}
fn collect_band_pixels(band: &RasterBuffer, width: u64, height: u64) -> Vec<f64> {
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
let v = band.get_pixel(x, y).unwrap_or(0.0);
data.push(v);
}
}
data
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::raster::calculator::ast::{BinaryOp, Expr, UnaryOp};
#[test]
fn test_compile_number_emits_load_const() {
let val = 12.5_f64;
let expr = Expr::Number(val);
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(ops.len(), 1, "expected exactly one opcode");
assert_eq!(
ops[0],
OpCode::LoadConst(0),
"first opcode must be LoadConst(0)"
);
assert!(
(constants[0] - val).abs() < f64::EPSILON,
"constant[0] must equal the compiled value"
);
}
#[test]
fn test_compile_band_emits_load_band() {
let expr = Expr::Band(0);
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0], OpCode::LoadBand(0));
}
#[test]
fn test_compile_add_binary() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(1.0)),
op: BinaryOp::Add,
right: Box::new(Expr::Number(2.0)),
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(ops.len(), 3, "expected [LoadConst, LoadConst, Add]");
assert!(matches!(ops[0], OpCode::LoadConst(_)));
assert!(matches!(ops[1], OpCode::LoadConst(_)));
assert_eq!(ops[2], OpCode::Add);
}
#[test]
fn test_compile_function_sqrt() {
let expr = Expr::Function {
name: "sqrt".to_string(),
args: vec![Expr::Band(0)],
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(ops.len(), 2, "expected [LoadBand(0), Sqrt]");
assert_eq!(ops[0], OpCode::LoadBand(0));
assert_eq!(ops[1], OpCode::Sqrt);
}
#[test]
fn test_compile_conditional() {
let expr = Expr::Conditional {
condition: Box::new(Expr::Number(1.0)),
then_expr: Box::new(Expr::Number(2.0)),
else_expr: Box::new(Expr::Number(3.0)),
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert!(ops.len() >= 2, "expected at least 2 opcodes");
assert_eq!(*ops.last().expect("ops must not be empty"), OpCode::Cond);
}
#[test]
fn test_estimate_stack_depth_simple_add() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(1.0)),
op: BinaryOp::Add,
right: Box::new(Expr::Number(2.0)),
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
let depth = estimate_stack_depth(&ops);
assert_eq!(depth, 2, "simple add needs stack depth 2");
}
#[test]
fn test_compile_unary_negate() {
let expr = Expr::UnaryOp {
op: UnaryOp::Negate,
expr: Box::new(Expr::Band(1)),
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(ops.len(), 2);
assert_eq!(ops[0], OpCode::LoadBand(1));
assert_eq!(ops[1], OpCode::Neg);
}
#[test]
fn test_constant_deduplication() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(1.0)),
op: BinaryOp::Add,
right: Box::new(Expr::Number(1.0)),
};
let mut constants = Vec::new();
let ops = compile_expr(&expr, &mut constants).expect("compile should succeed");
assert_eq!(constants.len(), 1, "1.0 should be stored once");
assert!(matches!(ops[0], OpCode::LoadConst(0)));
assert!(matches!(ops[1], OpCode::LoadConst(0)));
assert_eq!(ops[2], OpCode::Add);
}
#[test]
fn test_eval_bytecode_constant_inline() {
let prog = CompiledProgram {
ops: vec![OpCode::LoadConst(0)],
constants: vec![42.0],
required_bands: 0,
cache_slot_count: 0,
estimated_stack_depth: 1,
};
let mut cache = Vec::new();
let mut stack = Vec::with_capacity(4);
let result =
eval_bytecode(&prog, &[], 0, &mut cache, &mut stack).expect("eval should succeed");
assert!((result - 42.0).abs() < f64::EPSILON);
}
}