Skip to main content

oxigeo_algorithms/dsl/
macro_support.rs

1//! Macro support for embedding DSL source in Rust
2//!
3//! This module provides the `raster!` and `dsl_function!` macros. Both are
4//! ergonomic front-ends for writing raster-algebra DSL *source* inside Rust code.
5//!
6//! Note: despite living in a module historically named for "compile-time"
7//! evaluation, these macros do NOT evaluate the DSL at compile time. `raster!`
8//! `stringify!`s the tokens and parses/compiles them at run time (returning a
9//! [`CompiledProgram`](crate::dsl::CompiledProgram)); `dsl_function!` generates a
10//! helper that returns the function's DSL *source definition* as a `String`,
11//! ready to be prepended to a DSL program before the function is called.
12
13/// Build a [`CompiledProgram`](crate::dsl::CompiledProgram) from raster-algebra
14/// DSL tokens written inline in Rust.
15///
16/// The tokens are `stringify!`d and then parsed and compiled **at run time**
17/// (not at compile time). The macro expands to an expression of type
18/// `CompiledProgram`, so it must be used in a context where the `?` operator can
19/// propagate a parse error (i.e. a function returning
20/// [`Result`](crate::error::Result)).
21///
22/// # Examples
23///
24/// ```ignore
25/// use oxigeo_algorithms::raster;
26///
27/// // Simple NDVI calculation
28/// let ndvi = raster!((NIR - RED) / (NIR + RED));
29///
30/// // Complex multi-band analysis with conditions
31/// let result = raster! {
32///     let ndvi = (B8 - B4) / (B8 + B4);
33///     if ndvi > 0.6 then 1 else 0
34/// };
35/// ```
36#[macro_export]
37macro_rules! raster {
38    // Single expression
39    ($expr:expr) => {{
40        use $crate::dsl::ast::{Program, Statement};
41        use $crate::dsl::{parse_expression, CompiledProgram};
42
43        let expr_str = stringify!($expr);
44        let parsed = parse_expression(expr_str)?;
45        let program = Program {
46            statements: vec![Statement::Expr(Box::new(parsed))],
47        };
48        CompiledProgram::new(program)
49    }};
50
51    // Block with multiple statements
52    ({ $($stmt:stmt);+ }) => {{
53        use $crate::dsl::{parse_program, CompiledProgram};
54
55        let program_str = stringify!({ $($stmt);+ });
56        let parsed = parse_program(program_str)?;
57        CompiledProgram::new(parsed)
58    }};
59}
60
61/// Define a helper that returns the DSL *source definition* of a reusable
62/// raster-algebra function.
63///
64/// This does not register a Rust-callable function and does not evaluate
65/// anything: it expands to `pub fn <name>() -> String` returning a syntactically
66/// valid DSL function declaration such as
67/// `"fn ndvi(nir, red) = (nir - red) / (nir + red);"`. Prepend that string to a
68/// DSL program and then call the function from a DSL expression to use it.
69///
70/// # Examples
71///
72/// ```ignore
73/// use oxigeo_algorithms::dsl_function;
74/// use oxigeo_algorithms::dsl::RasterDsl;
75///
76/// dsl_function! {
77///     fn ndvi(nir, red) = (nir - red) / (nir + red);
78/// }
79///
80/// // `ndvi()` yields the DSL source; splice it into a program and call it.
81/// let program = format!("{}\nndvi(B1, B2);", ndvi());
82/// let dsl = RasterDsl::new();
83/// // dsl.execute(&program, &[nir_band, red_band]) -> resulting raster
84/// ```
85#[macro_export]
86macro_rules! dsl_function {
87    (fn $name:ident ( $($param:ident),* $(,)? ) = $body:expr ;) => {
88        /// Returns the DSL source definition for this function, suitable for
89        /// prepending to a DSL program before the function is called.
90        pub fn $name() -> ::std::string::String {
91            let params: &[&str] = &[ $( stringify!($param) ),* ];
92            let mut out = ::std::string::String::from("fn ");
93            out.push_str(stringify!($name));
94            out.push('(');
95            out.push_str(&params.join(", "));
96            out.push_str(") = ");
97            out.push_str(stringify!($body));
98            out.push(';');
99            out
100        }
101    };
102}
103
104#[cfg(test)]
105mod tests {
106    // Generate a helper via the macro at item scope.
107    dsl_function! {
108        fn ndvi(nir, red) = (nir - red) / (nir + red);
109    }
110
111    #[test]
112    fn test_dsl_function_emits_valid_source() {
113        // The generated helper returns a syntactically valid DSL definition
114        // (no trailing comma in the parameter list).
115        let src = ndvi();
116        assert_eq!(src, "fn ndvi(nir, red) = (nir - red) / (nir + red);");
117    }
118
119    #[test]
120    fn test_dsl_function_source_is_usable_in_a_program() {
121        use crate::dsl::{CompiledProgram, parse_program};
122        use oxigeo_core::buffer::RasterBuffer;
123        use oxigeo_core::types::RasterDataType;
124
125        // The emitted source must parse as a DSL program on its own...
126        let def = ndvi();
127        parse_program(&def).expect("generated dsl_function source must parse");
128
129        // ...and be callable end-to-end when spliced into a program and driven
130        // through the real compiler. (We use parse_program + CompiledProgram
131        // directly rather than RasterDsl::execute, whose expression fast-path
132        // would otherwise pick up only the trailing call statement.)
133        let program_src = format!("{def}\nndvi(B1, B2);");
134        let program = parse_program(&program_src).expect("full program must parse");
135        let compiled = CompiledProgram::new(program);
136
137        let mut nir = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
138        let mut red = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
139        for y in 0..2 {
140            for x in 0..2 {
141                nir.set_pixel(x, y, 3.0).expect("set nir");
142                red.set_pixel(x, y, 1.0).expect("set red");
143            }
144        }
145
146        let result = compiled
147            .execute(&[nir, red])
148            .expect("dsl_function program should execute");
149
150        // NDVI = (3 - 1) / (3 + 1) = 0.5 at every pixel.
151        for y in 0..2 {
152            for x in 0..2 {
153                let v = result.get_pixel(x, y).expect("pixel readable");
154                assert!((v - 0.5).abs() < 1e-6, "unexpected NDVI value {v}");
155            }
156        }
157    }
158}