Skip to main content

oxigdal_algorithms/dsl/
variables.rs

1//! Variable binding and environment management
2//!
3//! This module provides variable scoping and lookup for the DSL.
4
5use super::ast::{Expr, Type};
6use crate::error::{AlgorithmError, Result};
7use oxigdal_core::buffer::RasterBuffer;
8
9#[cfg(not(feature = "std"))]
10use alloc::{boxed::Box, collections::BTreeMap as HashMap, string::String, vec::Vec};
11
12#[cfg(feature = "std")]
13use std::collections::HashMap;
14
15/// Value type in the DSL runtime
16#[derive(Debug, Clone)]
17pub enum Value {
18    /// Numeric value
19    Number(f64),
20    /// Boolean value
21    Bool(bool),
22    /// Raster buffer
23    Raster(Box<RasterBuffer>),
24    /// Function closure
25    Function {
26        /// Function parameter names
27        params: Vec<String>,
28        /// Function body expression
29        body: Box<Expr>,
30        /// Captured environment
31        env: Environment,
32    },
33}
34
35impl Value {
36    /// Gets the type of this value
37    pub fn get_type(&self) -> Type {
38        match self {
39            Value::Number(_) => Type::Number,
40            Value::Bool(_) => Type::Bool,
41            Value::Raster(_) => Type::Raster,
42            Value::Function { .. } => Type::Unknown,
43        }
44    }
45
46    /// Converts value to f64 if possible
47    pub fn as_number(&self) -> Result<f64> {
48        match self {
49            Value::Number(n) => Ok(*n),
50            Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
51            _ => Err(AlgorithmError::InvalidParameter {
52                parameter: "value",
53                message: "Cannot convert to number".to_string(),
54            }),
55        }
56    }
57
58    /// Converts value to bool if possible
59    pub fn as_bool(&self) -> Result<bool> {
60        match self {
61            Value::Bool(b) => Ok(*b),
62            Value::Number(n) => Ok(n.abs() > f64::EPSILON),
63            _ => Err(AlgorithmError::InvalidParameter {
64                parameter: "value",
65                message: "Cannot convert to bool".to_string(),
66            }),
67        }
68    }
69
70    /// Gets raster buffer reference
71    pub fn as_raster(&self) -> Result<&RasterBuffer> {
72        match self {
73            Value::Raster(r) => Ok(r),
74            _ => Err(AlgorithmError::InvalidParameter {
75                parameter: "value",
76                message: "Not a raster".to_string(),
77            }),
78        }
79    }
80}
81
82/// Variable environment for scoping
83#[derive(Debug, Clone)]
84pub struct Environment {
85    /// Variable bindings in this scope
86    bindings: HashMap<String, Value>,
87    /// Parent scope (for nested scopes)
88    parent: Option<Box<Environment>>,
89}
90
91impl Default for Environment {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl Environment {
98    /// Creates a new empty environment
99    pub fn new() -> Self {
100        Self {
101            bindings: HashMap::new(),
102            parent: None,
103        }
104    }
105
106    /// Creates a new child environment
107    pub fn with_parent(parent: Environment) -> Self {
108        Self {
109            bindings: HashMap::new(),
110            parent: Some(Box::new(parent)),
111        }
112    }
113
114    /// Defines a variable in the current scope
115    pub fn define(&mut self, name: String, value: Value) {
116        self.bindings.insert(name, value);
117    }
118
119    /// Looks up a variable in this scope or parent scopes
120    pub fn lookup(&self, name: &str) -> Result<&Value> {
121        if let Some(value) = self.bindings.get(name) {
122            Ok(value)
123        } else if let Some(parent) = &self.parent {
124            parent.lookup(name)
125        } else {
126            Err(AlgorithmError::InvalidParameter {
127                parameter: "variable",
128                message: format!("Undefined variable: {name}"),
129            })
130        }
131    }
132
133    /// Checks if a variable is defined
134    pub fn is_defined(&self, name: &str) -> bool {
135        self.bindings.contains_key(name) || self.parent.as_ref().is_some_and(|p| p.is_defined(name))
136    }
137
138    /// Updates a variable in the current scope or parent scopes
139    pub fn update(&mut self, name: &str, value: Value) -> Result<()> {
140        if self.bindings.contains_key(name) {
141            self.bindings.insert(name.to_string(), value);
142            Ok(())
143        } else if let Some(parent) = &mut self.parent {
144            parent.update(name, value)
145        } else {
146            Err(AlgorithmError::InvalidParameter {
147                parameter: "variable",
148                message: format!("Undefined variable: {name}"),
149            })
150        }
151    }
152
153    /// Gets all variable names in this scope
154    pub fn variables(&self) -> Vec<String> {
155        self.bindings.keys().cloned().collect()
156    }
157
158    /// Gets the number of bindings in this scope (not including parents)
159    pub fn len(&self) -> usize {
160        self.bindings.len()
161    }
162
163    /// Checks if this environment is empty
164    pub fn is_empty(&self) -> bool {
165        self.bindings.is_empty()
166    }
167
168    /// Merges another environment into this one
169    pub fn merge(&mut self, other: Environment) {
170        for (name, value) in other.bindings {
171            self.bindings.insert(name, value);
172        }
173    }
174
175    /// Creates a snapshot of all bindings (flattened)
176    pub fn snapshot(&self) -> HashMap<String, Value> {
177        let mut result = HashMap::new();
178
179        // Add parent bindings first
180        if let Some(parent) = &self.parent {
181            for (k, v) in parent.snapshot() {
182                result.insert(k, v);
183            }
184        }
185
186        // Add our bindings (overriding parent)
187        for (k, v) in &self.bindings {
188            result.insert(k.clone(), v.clone());
189        }
190
191        result
192    }
193}
194
195/// Variable context for band references
196#[derive(Debug, Clone)]
197pub struct BandContext<'a> {
198    bands: &'a [RasterBuffer],
199}
200
201impl<'a> BandContext<'a> {
202    /// Creates a new band context
203    pub fn new(bands: &'a [RasterBuffer]) -> Self {
204        Self { bands }
205    }
206
207    /// Gets a band by index (1-based)
208    pub fn get_band(&self, index: usize) -> Result<&RasterBuffer> {
209        if index == 0 || index > self.bands.len() {
210            return Err(AlgorithmError::InvalidParameter {
211                parameter: "band",
212                message: format!("Band index {} out of range (1-{})", index, self.bands.len()),
213            });
214        }
215        Ok(&self.bands[index - 1])
216    }
217
218    /// Gets the number of bands
219    pub fn num_bands(&self) -> usize {
220        self.bands.len()
221    }
222
223    /// Checks if a band index is valid
224    pub fn is_valid_band(&self, index: usize) -> bool {
225        index > 0 && index <= self.bands.len()
226    }
227
228    /// Gets all bands
229    pub fn all_bands(&self) -> &[RasterBuffer] {
230        self.bands
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use oxigdal_core::types::RasterDataType;
238
239    #[test]
240    fn test_environment_define_lookup() {
241        let mut env = Environment::new();
242        env.define("x".to_string(), Value::Number(42.0));
243
244        let val = env.lookup("x").expect("Should find x");
245        assert!(matches!(val, Value::Number(n) if (n - 42.0).abs() < 1e-10));
246    }
247
248    #[test]
249    fn test_environment_parent() {
250        let mut parent = Environment::new();
251        parent.define("x".to_string(), Value::Number(10.0));
252
253        let mut child = Environment::with_parent(parent);
254        child.define("y".to_string(), Value::Number(20.0));
255
256        assert!(child.lookup("x").is_ok());
257        assert!(child.lookup("y").is_ok());
258        assert!(child.lookup("z").is_err());
259    }
260
261    #[test]
262    fn test_environment_update() {
263        let mut env = Environment::new();
264        env.define("x".to_string(), Value::Number(10.0));
265
266        let result = env.update("x", Value::Number(20.0));
267        assert!(result.is_ok());
268
269        let val = env.lookup("x").expect("Should find x");
270        assert!(matches!(val, Value::Number(n) if (n - 20.0).abs() < 1e-10));
271    }
272
273    #[test]
274    fn test_band_context() {
275        let bands = vec![
276            RasterBuffer::zeros(10, 10, RasterDataType::Float32),
277            RasterBuffer::zeros(10, 10, RasterDataType::Float32),
278        ];
279
280        let ctx = BandContext::new(&bands);
281        assert_eq!(ctx.num_bands(), 2);
282        assert!(ctx.is_valid_band(1));
283        assert!(ctx.is_valid_band(2));
284        assert!(!ctx.is_valid_band(0));
285        assert!(!ctx.is_valid_band(3));
286    }
287
288    #[test]
289    fn test_value_conversions() {
290        let num = Value::Number(42.5);
291        assert!((num.as_number().expect("Should convert") - 42.5).abs() < 1e-10);
292
293        let bool_val = Value::Bool(true);
294        assert!(bool_val.as_bool().expect("Should convert"));
295
296        let zero = Value::Number(0.0);
297        assert!(!zero.as_bool().expect("Should convert"));
298    }
299
300    #[test]
301    fn test_environment_snapshot() {
302        let mut parent = Environment::new();
303        parent.define("x".to_string(), Value::Number(10.0));
304
305        let mut child = Environment::with_parent(parent);
306        child.define("y".to_string(), Value::Number(20.0));
307
308        let snapshot = child.snapshot();
309        assert_eq!(snapshot.len(), 2);
310    }
311}