Skip to main content

oxigeo_algorithms/dsl/
optimizer.rs

1//! Expression optimizer for the DSL
2//!
3//! This module provides various optimization passes:
4//! - Constant folding
5//! - Common subexpression elimination
6//! - Dead code elimination
7//! - Algebraic simplifications
8
9use super::ast::{BinaryOp, Expr, Program, Statement, UnaryOp};
10
11#[cfg(not(feature = "std"))]
12use alloc::{
13    boxed::Box,
14    collections::{BTreeMap as HashMap, BTreeSet as HashSet},
15    format,
16    string::{String, ToString},
17    vec::Vec,
18};
19
20#[cfg(feature = "std")]
21use std::collections::{HashMap, HashSet};
22
23/// Optimization level
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum OptLevel {
26    /// No optimization
27    None,
28    /// Basic optimizations (constant folding)
29    Basic,
30    /// Standard optimizations (basic + algebraic simplifications)
31    Standard,
32    /// Aggressive optimizations (standard + CSE + DCE)
33    Aggressive,
34}
35
36/// Optimizer for DSL programs
37pub struct Optimizer {
38    level: OptLevel,
39}
40
41impl Default for Optimizer {
42    fn default() -> Self {
43        Self::new(OptLevel::Standard)
44    }
45}
46
47impl Optimizer {
48    /// Creates a new optimizer with the given optimization level
49    pub fn new(level: OptLevel) -> Self {
50        Self { level }
51    }
52
53    /// Optimizes a program
54    pub fn optimize_program(&self, mut program: Program) -> Program {
55        if self.level == OptLevel::None {
56            return program;
57        }
58
59        program.statements = program
60            .statements
61            .into_iter()
62            .map(|stmt| self.optimize_statement(stmt))
63            .collect();
64
65        // Dead code elimination: drop `let`/`fn` declarations that are never
66        // referenced (transitively) from an observable statement.
67        if self.level == OptLevel::Aggressive {
68            program = self.eliminate_dead_code(program);
69        }
70
71        program
72    }
73
74    /// Optimizes a single statement
75    pub fn optimize_statement(&self, stmt: Statement) -> Statement {
76        match stmt {
77            Statement::VariableDecl { name, value } => Statement::VariableDecl {
78                name,
79                value: Box::new(self.optimize_expr(*value)),
80            },
81            Statement::FunctionDecl { name, params, body } => Statement::FunctionDecl {
82                name,
83                params,
84                body: Box::new(self.optimize_expr(*body)),
85            },
86            Statement::Return(expr) => Statement::Return(Box::new(self.optimize_expr(*expr))),
87            Statement::Expr(expr) => Statement::Expr(Box::new(self.optimize_expr(*expr))),
88        }
89    }
90
91    /// Optimizes an expression
92    pub fn optimize_expr(&self, expr: Expr) -> Expr {
93        if self.level == OptLevel::None {
94            return expr;
95        }
96
97        let mut optimized = expr;
98
99        // Apply constant folding
100        optimized = self.constant_fold(optimized);
101
102        // Apply algebraic simplifications
103        if matches!(self.level, OptLevel::Standard | OptLevel::Aggressive) {
104            optimized = self.algebraic_simplify(optimized);
105        }
106
107        // Apply common subexpression elimination
108        if self.level == OptLevel::Aggressive {
109            optimized = self.eliminate_common_subexpressions(optimized);
110        }
111
112        optimized
113    }
114
115    /// Performs constant folding
116    fn constant_fold(&self, expr: Expr) -> Expr {
117        match expr {
118            Expr::Binary {
119                left,
120                op,
121                right,
122                ty,
123            } => {
124                let left_opt = self.constant_fold(*left);
125                let right_opt = self.constant_fold(*right);
126
127                if let (Expr::Number(l), Expr::Number(r)) = (&left_opt, &right_opt) {
128                    if let Some(result) = self.eval_const_binary(*l, op, *r) {
129                        return Expr::Number(result);
130                    }
131                }
132
133                Expr::Binary {
134                    left: Box::new(left_opt),
135                    op,
136                    right: Box::new(right_opt),
137                    ty,
138                }
139            }
140            Expr::Unary {
141                op,
142                expr: inner,
143                ty,
144            } => {
145                let inner_opt = self.constant_fold(*inner);
146
147                if let Expr::Number(n) = &inner_opt {
148                    if let Some(result) = self.eval_const_unary(op, *n) {
149                        return Expr::Number(result);
150                    }
151                }
152
153                Expr::Unary {
154                    op,
155                    expr: Box::new(inner_opt),
156                    ty,
157                }
158            }
159            Expr::Conditional {
160                condition,
161                then_expr,
162                else_expr,
163                ty,
164            } => {
165                let cond_opt = self.constant_fold(*condition);
166
167                // If condition is constant, return only the taken branch
168                if let Expr::Number(n) = &cond_opt {
169                    if n.abs() > f64::EPSILON {
170                        return self.constant_fold(*then_expr);
171                    } else {
172                        return self.constant_fold(*else_expr);
173                    }
174                }
175
176                Expr::Conditional {
177                    condition: Box::new(cond_opt),
178                    then_expr: Box::new(self.constant_fold(*then_expr)),
179                    else_expr: Box::new(self.constant_fold(*else_expr)),
180                    ty,
181                }
182            }
183            Expr::Call { name, args, ty } => Expr::Call {
184                name,
185                args: args
186                    .into_iter()
187                    .map(|arg| self.constant_fold(arg))
188                    .collect(),
189                ty,
190            },
191            Expr::Block {
192                statements,
193                result,
194                ty,
195            } => Expr::Block {
196                statements: statements
197                    .into_iter()
198                    .map(|stmt| self.optimize_statement(stmt))
199                    .collect(),
200                result: result.map(|r| Box::new(self.constant_fold(*r))),
201                ty,
202            },
203            _ => expr,
204        }
205    }
206
207    /// Evaluates a constant binary operation
208    fn eval_const_binary(&self, left: f64, op: BinaryOp, right: f64) -> Option<f64> {
209        let result = match op {
210            BinaryOp::Add => left + right,
211            BinaryOp::Subtract => left - right,
212            BinaryOp::Multiply => left * right,
213            BinaryOp::Divide => {
214                if right.abs() < f64::EPSILON {
215                    return None;
216                }
217                left / right
218            }
219            BinaryOp::Modulo => left % right,
220            BinaryOp::Power => left.powf(right),
221            BinaryOp::Equal => {
222                if (left - right).abs() < f64::EPSILON {
223                    1.0
224                } else {
225                    0.0
226                }
227            }
228            BinaryOp::NotEqual => {
229                if (left - right).abs() >= f64::EPSILON {
230                    1.0
231                } else {
232                    0.0
233                }
234            }
235            BinaryOp::Less => {
236                if left < right {
237                    1.0
238                } else {
239                    0.0
240                }
241            }
242            BinaryOp::LessEqual => {
243                if left <= right {
244                    1.0
245                } else {
246                    0.0
247                }
248            }
249            BinaryOp::Greater => {
250                if left > right {
251                    1.0
252                } else {
253                    0.0
254                }
255            }
256            BinaryOp::GreaterEqual => {
257                if left >= right {
258                    1.0
259                } else {
260                    0.0
261                }
262            }
263            BinaryOp::And => {
264                if left != 0.0 && right != 0.0 {
265                    1.0
266                } else {
267                    0.0
268                }
269            }
270            BinaryOp::Or => {
271                if left != 0.0 || right != 0.0 {
272                    1.0
273                } else {
274                    0.0
275                }
276            }
277        };
278
279        Some(result)
280    }
281
282    /// Evaluates a constant unary operation
283    fn eval_const_unary(&self, op: UnaryOp, operand: f64) -> Option<f64> {
284        let result = match op {
285            UnaryOp::Negate => -operand,
286            UnaryOp::Plus => operand,
287            UnaryOp::Not => {
288                if operand.abs() < f64::EPSILON {
289                    1.0
290                } else {
291                    0.0
292                }
293            }
294        };
295
296        Some(result)
297    }
298
299    /// Performs algebraic simplifications
300    fn algebraic_simplify(&self, expr: Expr) -> Expr {
301        match expr {
302            Expr::Binary {
303                left,
304                op,
305                right,
306                ty,
307            } => {
308                let left_opt = self.algebraic_simplify(*left);
309                let right_opt = self.algebraic_simplify(*right);
310
311                // x + 0 = x
312                if op == BinaryOp::Add {
313                    if let Expr::Number(n) = &right_opt {
314                        if n.abs() < f64::EPSILON {
315                            return left_opt;
316                        }
317                    }
318                    if let Expr::Number(n) = &left_opt {
319                        if n.abs() < f64::EPSILON {
320                            return right_opt;
321                        }
322                    }
323                }
324
325                // x - 0 = x
326                if op == BinaryOp::Subtract {
327                    if let Expr::Number(n) = &right_opt {
328                        if n.abs() < f64::EPSILON {
329                            return left_opt;
330                        }
331                    }
332                }
333
334                // NOTE: `x * 0 = 0` and `0 * x = 0` are intentionally NOT simplified here.
335                // Per IEEE-754, `NaN * 0.0 == NaN` and `Inf * 0.0 == NaN`, not `0.0`. This
336                // crate's raster-algebra evaluator uses NaN as a NoData sentinel (e.g. from
337                // division by near-zero), so folding `x * 0` to a constant `0.0` would
338                // silently turn per-pixel NoData into a false constant zero. See the
339                // `test_algebraic_simplify_mul_zero_preserves_nan_semantics` regression test.
340
341                // x * 1 = x
342                if op == BinaryOp::Multiply {
343                    if let Expr::Number(n) = &right_opt {
344                        if (n - 1.0).abs() < f64::EPSILON {
345                            return left_opt;
346                        }
347                    }
348                    if let Expr::Number(n) = &left_opt {
349                        if (n - 1.0).abs() < f64::EPSILON {
350                            return right_opt;
351                        }
352                    }
353                }
354
355                // x / 1 = x
356                if op == BinaryOp::Divide {
357                    if let Expr::Number(n) = &right_opt {
358                        if (n - 1.0).abs() < f64::EPSILON {
359                            return left_opt;
360                        }
361                    }
362                }
363
364                // x ^ 0 = 1
365                if op == BinaryOp::Power {
366                    if let Expr::Number(n) = &right_opt {
367                        if n.abs() < f64::EPSILON {
368                            return Expr::Number(1.0);
369                        }
370                    }
371                }
372
373                // x ^ 1 = x
374                if op == BinaryOp::Power {
375                    if let Expr::Number(n) = &right_opt {
376                        if (n - 1.0).abs() < f64::EPSILON {
377                            return left_opt;
378                        }
379                    }
380                }
381
382                Expr::Binary {
383                    left: Box::new(left_opt),
384                    op,
385                    right: Box::new(right_opt),
386                    ty,
387                }
388            }
389            Expr::Unary {
390                op,
391                expr: inner,
392                ty,
393            } => {
394                let inner_opt = self.algebraic_simplify(*inner);
395
396                // --x = x
397                if op == UnaryOp::Negate {
398                    if let Expr::Unary {
399                        op: UnaryOp::Negate,
400                        expr: double_neg,
401                        ..
402                    } = &inner_opt
403                    {
404                        return *double_neg.clone();
405                    }
406                }
407
408                // +x = x
409                if op == UnaryOp::Plus {
410                    return inner_opt;
411                }
412
413                Expr::Unary {
414                    op,
415                    expr: Box::new(inner_opt),
416                    ty,
417                }
418            }
419            Expr::Conditional {
420                condition,
421                then_expr,
422                else_expr,
423                ty,
424            } => Expr::Conditional {
425                condition: Box::new(self.algebraic_simplify(*condition)),
426                then_expr: Box::new(self.algebraic_simplify(*then_expr)),
427                else_expr: Box::new(self.algebraic_simplify(*else_expr)),
428                ty,
429            },
430            Expr::Call { name, args, ty } => Expr::Call {
431                name,
432                args: args
433                    .into_iter()
434                    .map(|arg| self.algebraic_simplify(arg))
435                    .collect(),
436                ty,
437            },
438            Expr::Block {
439                statements,
440                result,
441                ty,
442            } => Expr::Block {
443                statements: statements
444                    .into_iter()
445                    .map(|stmt| self.optimize_statement(stmt))
446                    .collect(),
447                result: result.map(|r| Box::new(self.algebraic_simplify(*r))),
448                ty,
449            },
450            _ => expr,
451        }
452    }
453
454    /// Eliminates common subexpressions.
455    ///
456    /// Repeated, side-effect-free subexpressions (those built solely from band
457    /// references and numeric literals, so they carry no free variables) are
458    /// hoisted into `let` bindings inside a wrapping [`Expr::Block`], and every
459    /// occurrence is rewritten to reference the bound variable. This turns
460    /// `(B1 - B2) / (B1 + B2)`-style algebra where the same subtree recurs into a
461    /// single evaluation of each shared subtree.
462    ///
463    /// Only "hoistable" subexpressions are considered (no `Variable`, `Block` or
464    /// `ForLoop` nodes anywhere inside), which guarantees the hoisted binding is
465    /// valid in the outermost scope and cannot capture an inner-scope variable.
466    fn eliminate_common_subexpressions(&self, expr: Expr) -> Expr {
467        // 1. Count every hoistable, non-trivial subexpression by structural key.
468        let mut counts: HashMap<String, usize> = HashMap::new();
469        let mut reprs: HashMap<String, Expr> = HashMap::new();
470        Self::collect_cse_candidates(&expr, &mut counts, &mut reprs);
471
472        // 2. Keep only subexpressions that occur at least twice.
473        let mut candidates: Vec<(String, Expr)> = counts
474            .iter()
475            .filter(|&(_, &c)| c >= 2)
476            .filter_map(|(k, _)| reprs.get(k).map(|e| (k.clone(), e.clone())))
477            .collect();
478
479        if candidates.is_empty() {
480            return expr;
481        }
482
483        // 3. Bind smaller (inner) subexpressions first so that a larger binding's
484        //    definition can reference the variables of its nested subexpressions.
485        //    Deterministic ordering via (size, structural key).
486        candidates.sort_by(|(ka, ea), (kb, eb)| {
487            Self::expr_size(ea)
488                .cmp(&Self::expr_size(eb))
489                .then_with(|| ka.cmp(kb))
490        });
491
492        // 4. Assign a fresh, collision-free binding name to each candidate key.
493        let mut name_map: HashMap<String, String> = HashMap::new();
494        for (i, (key, _)) in candidates.iter().enumerate() {
495            name_map.insert(key.clone(), format!("__cse_{i}"));
496        }
497
498        // 5. Emit `let` bindings in dependency order. A binding's definition
499        //    rewrites its *children* (never the node itself, which would produce
500        //    `let x = x;`) so nested common subexpressions reference earlier binds.
501        let mut statements: Vec<Statement> = Vec::with_capacity(candidates.len());
502        for (key, repr) in &candidates {
503            // `key` is guaranteed present in `name_map` (inserted above).
504            let name = name_map.get(key).cloned().unwrap_or_default();
505            let definition = Self::rewrite_children_with_cse(repr, &name_map);
506            statements.push(Statement::VariableDecl {
507                name,
508                value: Box::new(definition),
509            });
510        }
511
512        // 6. Rewrite the body: every candidate occurrence becomes its variable.
513        let ty = expr.get_type();
514        let body = Self::rewrite_with_cse(&expr, &name_map);
515
516        Expr::Block {
517            statements,
518            result: Some(Box::new(body)),
519            ty,
520        }
521    }
522
523    /// Recursively collects hoistable, non-trivial subexpressions with their
524    /// occurrence counts (keyed by structural fingerprint) and a representative.
525    fn collect_cse_candidates(
526        expr: &Expr,
527        counts: &mut HashMap<String, usize>,
528        reprs: &mut HashMap<String, Expr>,
529    ) {
530        if Self::is_cse_target(expr) {
531            let key = Self::cse_key(expr);
532            *counts.entry(key.clone()).or_insert(0) += 1;
533            reprs.entry(key).or_insert_with(|| expr.clone());
534        }
535
536        match expr {
537            Expr::Binary { left, right, .. } => {
538                Self::collect_cse_candidates(left, counts, reprs);
539                Self::collect_cse_candidates(right, counts, reprs);
540            }
541            Expr::Unary { expr: inner, .. } => {
542                Self::collect_cse_candidates(inner, counts, reprs);
543            }
544            Expr::Call { args, .. } => {
545                for arg in args {
546                    Self::collect_cse_candidates(arg, counts, reprs);
547                }
548            }
549            Expr::Conditional {
550                condition,
551                then_expr,
552                else_expr,
553                ..
554            } => {
555                Self::collect_cse_candidates(condition, counts, reprs);
556                Self::collect_cse_candidates(then_expr, counts, reprs);
557                Self::collect_cse_candidates(else_expr, counts, reprs);
558            }
559            Expr::Block {
560                statements, result, ..
561            } => {
562                for stmt in statements {
563                    Self::collect_cse_candidates_stmt(stmt, counts, reprs);
564                }
565                if let Some(result) = result {
566                    Self::collect_cse_candidates(result, counts, reprs);
567                }
568            }
569            Expr::ForLoop {
570                start, end, body, ..
571            } => {
572                Self::collect_cse_candidates(start, counts, reprs);
573                Self::collect_cse_candidates(end, counts, reprs);
574                Self::collect_cse_candidates(body, counts, reprs);
575            }
576            Expr::Number(_) | Expr::Band(_) | Expr::Variable(_) => {}
577        }
578    }
579
580    fn collect_cse_candidates_stmt(
581        stmt: &Statement,
582        counts: &mut HashMap<String, usize>,
583        reprs: &mut HashMap<String, Expr>,
584    ) {
585        match stmt {
586            Statement::VariableDecl { value, .. } => {
587                Self::collect_cse_candidates(value, counts, reprs);
588            }
589            Statement::FunctionDecl { body, .. } => {
590                Self::collect_cse_candidates(body, counts, reprs);
591            }
592            Statement::Return(expr) | Statement::Expr(expr) => {
593                Self::collect_cse_candidates(expr, counts, reprs);
594            }
595        }
596    }
597
598    /// A subexpression is a CSE target if it is non-trivial (worth caching) and
599    /// hoistable (references only bands/literals, never a free variable, block,
600    /// or loop, so it is valid to evaluate once in the outermost scope).
601    fn is_cse_target(expr: &Expr) -> bool {
602        matches!(
603            expr,
604            Expr::Binary { .. } | Expr::Unary { .. } | Expr::Call { .. } | Expr::Conditional { .. }
605        ) && Self::is_hoistable(expr)
606    }
607
608    /// Whether the entire subtree can be safely evaluated in the outermost scope.
609    fn is_hoistable(expr: &Expr) -> bool {
610        match expr {
611            Expr::Number(_) | Expr::Band(_) => true,
612            Expr::Variable(_) | Expr::Block { .. } | Expr::ForLoop { .. } => false,
613            Expr::Binary { left, right, .. } => {
614                Self::is_hoistable(left) && Self::is_hoistable(right)
615            }
616            Expr::Unary { expr: inner, .. } => Self::is_hoistable(inner),
617            Expr::Call { args, .. } => args.iter().all(Self::is_hoistable),
618            Expr::Conditional {
619                condition,
620                then_expr,
621                else_expr,
622                ..
623            } => {
624                Self::is_hoistable(condition)
625                    && Self::is_hoistable(then_expr)
626                    && Self::is_hoistable(else_expr)
627            }
628        }
629    }
630
631    /// Structural fingerprint used to identify equal subexpressions.
632    fn cse_key(expr: &Expr) -> String {
633        format!("{expr:?}")
634    }
635
636    /// Node count of a subtree (used to order bindings inner-first).
637    fn expr_size(expr: &Expr) -> usize {
638        1 + match expr {
639            Expr::Number(_) | Expr::Band(_) | Expr::Variable(_) => 0,
640            Expr::Binary { left, right, .. } => Self::expr_size(left) + Self::expr_size(right),
641            Expr::Unary { expr: inner, .. } => Self::expr_size(inner),
642            Expr::Call { args, .. } => args.iter().map(Self::expr_size).sum(),
643            Expr::Conditional {
644                condition,
645                then_expr,
646                else_expr,
647                ..
648            } => {
649                Self::expr_size(condition) + Self::expr_size(then_expr) + Self::expr_size(else_expr)
650            }
651            Expr::Block {
652                statements, result, ..
653            } => {
654                statements.iter().map(Self::stmt_size).sum::<usize>()
655                    + result.as_ref().map_or(0, |r| Self::expr_size(r))
656            }
657            Expr::ForLoop {
658                start, end, body, ..
659            } => Self::expr_size(start) + Self::expr_size(end) + Self::expr_size(body),
660        }
661    }
662
663    fn stmt_size(stmt: &Statement) -> usize {
664        match stmt {
665            Statement::VariableDecl { value, .. } => Self::expr_size(value),
666            Statement::FunctionDecl { body, .. } => Self::expr_size(body),
667            Statement::Return(expr) | Statement::Expr(expr) => Self::expr_size(expr),
668        }
669    }
670
671    /// Rewrites an expression, replacing any subexpression whose structural key is
672    /// bound in `name_map` with a reference to its cached variable.
673    fn rewrite_with_cse(expr: &Expr, name_map: &HashMap<String, String>) -> Expr {
674        let key = Self::cse_key(expr);
675        if let Some(name) = name_map.get(&key) {
676            return Expr::Variable(name.clone());
677        }
678        Self::rewrite_children_with_cse(expr, name_map)
679    }
680
681    /// Like [`Self::rewrite_with_cse`] but never replaces the node itself, only
682    /// its descendants (used when building a binding's own definition).
683    fn rewrite_children_with_cse(expr: &Expr, name_map: &HashMap<String, String>) -> Expr {
684        match expr {
685            Expr::Binary {
686                left,
687                op,
688                right,
689                ty,
690            } => Expr::Binary {
691                left: Box::new(Self::rewrite_with_cse(left, name_map)),
692                op: *op,
693                right: Box::new(Self::rewrite_with_cse(right, name_map)),
694                ty: *ty,
695            },
696            Expr::Unary {
697                op,
698                expr: inner,
699                ty,
700            } => Expr::Unary {
701                op: *op,
702                expr: Box::new(Self::rewrite_with_cse(inner, name_map)),
703                ty: *ty,
704            },
705            Expr::Call { name, args, ty } => Expr::Call {
706                name: name.clone(),
707                args: args
708                    .iter()
709                    .map(|a| Self::rewrite_with_cse(a, name_map))
710                    .collect(),
711                ty: *ty,
712            },
713            Expr::Conditional {
714                condition,
715                then_expr,
716                else_expr,
717                ty,
718            } => Expr::Conditional {
719                condition: Box::new(Self::rewrite_with_cse(condition, name_map)),
720                then_expr: Box::new(Self::rewrite_with_cse(then_expr, name_map)),
721                else_expr: Box::new(Self::rewrite_with_cse(else_expr, name_map)),
722                ty: *ty,
723            },
724            // Leaves and non-hoistable containers are returned unchanged. (CSE
725            // targets never live inside Block/ForLoop bindings emitted here, so we
726            // do not descend into them; the outer body rewrite handles those.)
727            other => other.clone(),
728        }
729    }
730
731    /// Removes dead `let`/`fn` declarations: a declaration is retained only if its
732    /// bound name is referenced, directly or transitively, from an observable
733    /// statement (a `return` or a bare expression statement).
734    fn eliminate_dead_code(&self, mut program: Program) -> Program {
735        // Names referenced by each declaration's definition.
736        let mut decl_refs: HashMap<String, Vec<String>> = HashMap::new();
737        for stmt in &program.statements {
738            match stmt {
739                Statement::VariableDecl { name, value } => {
740                    let refs = Self::referenced_names(value);
741                    decl_refs.entry(name.clone()).or_default().extend(refs);
742                }
743                Statement::FunctionDecl { name, params, body } => {
744                    let mut refs = Self::referenced_names(body);
745                    // Parameters are locally bound, not free references.
746                    refs.retain(|n| !params.contains(n));
747                    decl_refs.entry(name.clone()).or_default().extend(refs);
748                }
749                _ => {}
750            }
751        }
752
753        // Seed the live set from observable statements.
754        let mut live: HashSet<String> = HashSet::new();
755        let mut worklist: Vec<String> = Vec::new();
756        for stmt in &program.statements {
757            if let Statement::Return(expr) | Statement::Expr(expr) = stmt {
758                for name in Self::referenced_names(expr) {
759                    if live.insert(name.clone()) {
760                        worklist.push(name);
761                    }
762                }
763            }
764        }
765
766        // Transitive closure over declaration references.
767        while let Some(name) = worklist.pop() {
768            if let Some(refs) = decl_refs.get(&name).cloned() {
769                for r in refs {
770                    if live.insert(r.clone()) {
771                        worklist.push(r);
772                    }
773                }
774            }
775        }
776
777        // Keep observable statements; keep declarations whose name is live.
778        program.statements.retain(|stmt| match stmt {
779            Statement::VariableDecl { name, .. } | Statement::FunctionDecl { name, .. } => {
780                live.contains(name)
781            }
782            Statement::Return(_) | Statement::Expr(_) => true,
783        });
784
785        program
786    }
787
788    /// Collects the free variable and function-call names referenced by an expr.
789    fn referenced_names(expr: &Expr) -> Vec<String> {
790        let mut out = Vec::new();
791        Self::collect_referenced_names(expr, &mut out);
792        out
793    }
794
795    fn collect_referenced_names(expr: &Expr, out: &mut Vec<String>) {
796        match expr {
797            Expr::Variable(name) => out.push(name.clone()),
798            Expr::Number(_) | Expr::Band(_) => {}
799            Expr::Binary { left, right, .. } => {
800                Self::collect_referenced_names(left, out);
801                Self::collect_referenced_names(right, out);
802            }
803            Expr::Unary { expr: inner, .. } => Self::collect_referenced_names(inner, out),
804            Expr::Call { name, args, .. } => {
805                out.push(name.clone());
806                for arg in args {
807                    Self::collect_referenced_names(arg, out);
808                }
809            }
810            Expr::Conditional {
811                condition,
812                then_expr,
813                else_expr,
814                ..
815            } => {
816                Self::collect_referenced_names(condition, out);
817                Self::collect_referenced_names(then_expr, out);
818                Self::collect_referenced_names(else_expr, out);
819            }
820            Expr::Block {
821                statements, result, ..
822            } => {
823                for stmt in statements {
824                    Self::collect_referenced_names_stmt(stmt, out);
825                }
826                if let Some(result) = result {
827                    Self::collect_referenced_names(result, out);
828                }
829            }
830            Expr::ForLoop {
831                start, end, body, ..
832            } => {
833                Self::collect_referenced_names(start, out);
834                Self::collect_referenced_names(end, out);
835                Self::collect_referenced_names(body, out);
836            }
837        }
838    }
839
840    fn collect_referenced_names_stmt(stmt: &Statement, out: &mut Vec<String>) {
841        match stmt {
842            Statement::VariableDecl { value, .. } => Self::collect_referenced_names(value, out),
843            Statement::FunctionDecl { body, .. } => Self::collect_referenced_names(body, out),
844            Statement::Return(expr) | Statement::Expr(expr) => {
845                Self::collect_referenced_names(expr, out)
846            }
847        }
848    }
849}
850
851#[cfg(test)]
852#[allow(clippy::panic)]
853mod tests {
854    use super::*;
855    use crate::dsl::Type;
856
857    #[test]
858    fn test_constant_fold_add() {
859        let expr = Expr::Binary {
860            left: Box::new(Expr::Number(2.0)),
861            op: BinaryOp::Add,
862            right: Box::new(Expr::Number(3.0)),
863            ty: Type::Number,
864        };
865
866        let opt = Optimizer::new(OptLevel::Basic);
867        let result = opt.optimize_expr(expr);
868
869        assert!(matches!(result, Expr::Number(n) if (n - 5.0).abs() < 1e-10));
870    }
871
872    #[test]
873    fn test_constant_fold_nested() {
874        let expr = Expr::Binary {
875            left: Box::new(Expr::Binary {
876                left: Box::new(Expr::Number(2.0)),
877                op: BinaryOp::Multiply,
878                right: Box::new(Expr::Number(3.0)),
879                ty: Type::Number,
880            }),
881            op: BinaryOp::Add,
882            right: Box::new(Expr::Number(4.0)),
883            ty: Type::Number,
884        };
885
886        let opt = Optimizer::new(OptLevel::Basic);
887        let result = opt.optimize_expr(expr);
888
889        assert!(matches!(result, Expr::Number(n) if (n - 10.0).abs() < 1e-10));
890    }
891
892    #[test]
893    fn test_algebraic_simplify_add_zero() {
894        let expr = Expr::Binary {
895            left: Box::new(Expr::Band(1)),
896            op: BinaryOp::Add,
897            right: Box::new(Expr::Number(0.0)),
898            ty: Type::Raster,
899        };
900
901        let opt = Optimizer::new(OptLevel::Standard);
902        let result = opt.optimize_expr(expr);
903
904        assert!(matches!(result, Expr::Band(1)));
905    }
906
907    #[test]
908    fn test_algebraic_simplify_mul_one() {
909        let expr = Expr::Binary {
910            left: Box::new(Expr::Band(1)),
911            op: BinaryOp::Multiply,
912            right: Box::new(Expr::Number(1.0)),
913            ty: Type::Raster,
914        };
915
916        let opt = Optimizer::new(OptLevel::Standard);
917        let result = opt.optimize_expr(expr);
918
919        assert!(matches!(result, Expr::Band(1)));
920    }
921
922    #[test]
923    fn test_algebraic_simplify_mul_zero_not_folded() {
924        // `x * 0` and `0 * x` must NOT be simplified to a constant `Number(0.0)`.
925        // Per IEEE-754, NaN * 0.0 == NaN and Inf * 0.0 == NaN (not 0.0), and this
926        // crate's raster-algebra evaluator relies on NaN as a NoData sentinel.
927        // Folding this to a constant would silently turn per-pixel NoData into 0.0.
928        let expr = Expr::Binary {
929            left: Box::new(Expr::Band(1)),
930            op: BinaryOp::Multiply,
931            right: Box::new(Expr::Number(0.0)),
932            ty: Type::Raster,
933        };
934
935        let opt = Optimizer::new(OptLevel::Standard);
936        let result = opt.optimize_expr(expr);
937
938        // Must remain a Binary(Band(1) * 0), not collapse to Number(0.0).
939        assert!(matches!(
940            result,
941            Expr::Binary {
942                op: BinaryOp::Multiply,
943                ..
944            }
945        ));
946        assert!(!matches!(result, Expr::Number(_)));
947
948        // Same for the commuted form `0 * x`.
949        let expr_commuted = Expr::Binary {
950            left: Box::new(Expr::Number(0.0)),
951            op: BinaryOp::Multiply,
952            right: Box::new(Expr::Band(1)),
953            ty: Type::Raster,
954        };
955        let result_commuted = opt.optimize_expr(expr_commuted);
956        assert!(!matches!(result_commuted, Expr::Number(_)));
957    }
958
959    #[test]
960    fn test_algebraic_simplify_mul_zero_preserves_nan_semantics() {
961        // Regression test: `(B1 / B2) * 0` must NOT silently turn per-pixel NoData
962        // (NaN, produced by division-by-near-zero) into a false constant `0.0`.
963        //
964        // The evaluator treats NaN as the NoData sentinel; the algebraic simplifier
965        // must not fold `x * 0` to `0` since `NaN * 0.0 == NaN` (and `Inf * 0.0 ==
966        // NaN`) under IEEE-754, not `0.0`.
967        use crate::dsl::RasterDsl;
968        use oxigeo_core::buffer::RasterBuffer;
969        use oxigeo_core::types::RasterDataType;
970
971        let mut b1 = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
972        let mut b2 = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
973        // B1 = 1.0, B2 = 0.0 => B1 / B2 evaluates to NaN (NoData) in this evaluator.
974        assert!(b1.set_pixel(0, 0, 1.0).is_ok());
975        assert!(b2.set_pixel(0, 0, 0.0).is_ok());
976
977        let dsl = RasterDsl::new();
978        let result = dsl
979            .execute("(B1 / B2) * 0", &[b1, b2])
980            .expect("DSL expression should execute successfully");
981
982        let pixel = result
983            .get_pixel(0, 0)
984            .expect("pixel (0, 0) should be readable");
985
986        assert!(
987            pixel.is_nan(),
988            "expected NoData (NaN) to survive `* 0`, got {pixel} instead"
989        );
990    }
991
992    #[test]
993    fn test_double_negation() {
994        let expr = Expr::Unary {
995            op: UnaryOp::Negate,
996            expr: Box::new(Expr::Unary {
997                op: UnaryOp::Negate,
998                expr: Box::new(Expr::Band(1)),
999                ty: Type::Raster,
1000            }),
1001            ty: Type::Raster,
1002        };
1003
1004        let opt = Optimizer::new(OptLevel::Standard);
1005        let result = opt.optimize_expr(expr);
1006
1007        assert!(matches!(result, Expr::Band(1)));
1008    }
1009
1010    #[test]
1011    fn test_unary_plus() {
1012        let expr = Expr::Unary {
1013            op: UnaryOp::Plus,
1014            expr: Box::new(Expr::Band(1)),
1015            ty: Type::Raster,
1016        };
1017
1018        let opt = Optimizer::new(OptLevel::Standard);
1019        let result = opt.optimize_expr(expr);
1020
1021        assert!(matches!(result, Expr::Band(1)));
1022    }
1023
1024    /// Builds `(B1 + B2)` with a given band pair.
1025    fn add_bands(a: usize, b: usize) -> Expr {
1026        Expr::Binary {
1027            left: Box::new(Expr::Band(a)),
1028            op: BinaryOp::Add,
1029            right: Box::new(Expr::Band(b)),
1030            ty: Type::Raster,
1031        }
1032    }
1033
1034    #[test]
1035    fn test_cse_hoists_repeated_subexpression() {
1036        // (B1 + B2) * (B1 + B2)  =>  { let __cse_0 = B1 + B2; __cse_0 * __cse_0 }
1037        let expr = Expr::Binary {
1038            left: Box::new(add_bands(1, 2)),
1039            op: BinaryOp::Multiply,
1040            right: Box::new(add_bands(1, 2)),
1041            ty: Type::Raster,
1042        };
1043
1044        let opt = Optimizer::new(OptLevel::Aggressive);
1045        let result = opt.optimize_expr(expr);
1046
1047        // Result must be a Block with exactly one hoisted binding.
1048        let Expr::Block {
1049            statements, result, ..
1050        } = result
1051        else {
1052            panic!("expected CSE to produce a Block, got {result:?}");
1053        };
1054        assert_eq!(statements.len(), 1, "one common subexpression expected");
1055        let Statement::VariableDecl { name, value } = &statements[0] else {
1056            panic!("expected a VariableDecl binding");
1057        };
1058        // The binding must be the shared `B1 + B2` subtree.
1059        assert!(matches!(
1060            **value,
1061            Expr::Binary {
1062                op: BinaryOp::Add,
1063                ..
1064            }
1065        ));
1066        // The body must reference the bound variable on both sides (no B1+B2 left).
1067        let body = result.expect("block must have a result");
1068        let Expr::Binary { left, right, .. } = *body else {
1069            panic!("expected a Binary body");
1070        };
1071        assert!(matches!(*left, Expr::Variable(ref n) if n == name));
1072        assert!(matches!(*right, Expr::Variable(ref n) if n == name));
1073    }
1074
1075    #[test]
1076    fn test_cse_no_hoist_when_unique() {
1077        // (B1 + B2) has no repeated subexpression -> unchanged (no Block wrapper).
1078        let expr = add_bands(1, 2);
1079        let opt = Optimizer::new(OptLevel::Aggressive);
1080        let result = opt.optimize_expr(expr);
1081        assert!(
1082            matches!(result, Expr::Binary { .. }),
1083            "unique expression must not be wrapped in a CSE block"
1084        );
1085    }
1086
1087    #[test]
1088    fn test_cse_nested_common_subexpressions() {
1089        // ((B1 + B2) * B3) + ((B1 + B2) * B3)
1090        // Both the inner (B1 + B2) and the larger ((B1 + B2) * B3) repeat, so two
1091        // bindings are produced with the larger one referencing the smaller.
1092        let times_b3 = |()| Expr::Binary {
1093            left: Box::new(add_bands(1, 2)),
1094            op: BinaryOp::Multiply,
1095            right: Box::new(Expr::Band(3)),
1096            ty: Type::Raster,
1097        };
1098        let expr = Expr::Binary {
1099            left: Box::new(times_b3(())),
1100            op: BinaryOp::Add,
1101            right: Box::new(times_b3(())),
1102            ty: Type::Raster,
1103        };
1104
1105        let opt = Optimizer::new(OptLevel::Aggressive);
1106        let result = opt.optimize_expr(expr);
1107
1108        let Expr::Block { statements, .. } = result else {
1109            panic!("expected a CSE block");
1110        };
1111        assert_eq!(statements.len(), 2, "inner and outer CSE expected");
1112        // First binding (inner, smaller) is `B1 + B2`.
1113        let Statement::VariableDecl {
1114            name: inner_name,
1115            value: inner_value,
1116        } = &statements[0]
1117        else {
1118            panic!("expected inner binding");
1119        };
1120        assert!(matches!(
1121            **inner_value,
1122            Expr::Binary {
1123                op: BinaryOp::Add,
1124                ..
1125            }
1126        ));
1127        // Second binding (outer) must reference the inner binding, not re-inline
1128        // `B1 + B2`.
1129        let Statement::VariableDecl {
1130            value: outer_value, ..
1131        } = &statements[1]
1132        else {
1133            panic!("expected outer binding");
1134        };
1135        let Expr::Binary { left, .. } = &**outer_value else {
1136            panic!("expected outer binding to be a multiply");
1137        };
1138        assert!(
1139            matches!(**left, Expr::Variable(ref n) if n == inner_name),
1140            "outer binding must reference the inner CSE variable"
1141        );
1142    }
1143
1144    #[test]
1145    fn test_cse_preserves_result_value() {
1146        use crate::dsl::RasterDsl;
1147        use oxigeo_core::buffer::RasterBuffer;
1148        use oxigeo_core::types::RasterDataType;
1149
1150        let mut b1 = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
1151        let mut b2 = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
1152        for y in 0..2 {
1153            for x in 0..2 {
1154                assert!(b1.set_pixel(x, y, 3.0).is_ok());
1155                assert!(b2.set_pixel(x, y, 1.0).is_ok());
1156            }
1157        }
1158
1159        // (B1 - B2) / (B1 + B2) => (3-1)/(3+1) = 0.5 at every pixel, with and
1160        // without CSE the value must be identical.
1161        let expr = "(B1 - B2) / (B1 + B2)";
1162
1163        let mut plain = RasterDsl::new();
1164        plain.set_opt_level(OptLevel::None);
1165        let mut aggressive = RasterDsl::new();
1166        aggressive.set_opt_level(OptLevel::Aggressive);
1167
1168        let r_plain = plain
1169            .execute(expr, &[b1.clone(), b2.clone()])
1170            .expect("plain execution should succeed");
1171        let r_cse = aggressive
1172            .execute(expr, &[b1, b2])
1173            .expect("aggressive execution should succeed");
1174
1175        for y in 0..2 {
1176            for x in 0..2 {
1177                let a = r_plain.get_pixel(x, y).expect("pixel readable");
1178                let b = r_cse.get_pixel(x, y).expect("pixel readable");
1179                assert!((a - b).abs() < 1e-6, "CSE changed the value: {a} vs {b}");
1180                assert!((a - 0.5).abs() < 1e-6);
1181            }
1182        }
1183    }
1184
1185    #[test]
1186    fn test_dce_removes_unused_declaration() {
1187        // let used = B1 + B2;
1188        // let dead = B1 * B2;   // never referenced
1189        // used;
1190        let program = Program {
1191            statements: vec![
1192                Statement::VariableDecl {
1193                    name: "used".to_string(),
1194                    value: Box::new(add_bands(1, 2)),
1195                },
1196                Statement::VariableDecl {
1197                    name: "dead".to_string(),
1198                    value: Box::new(Expr::Binary {
1199                        left: Box::new(Expr::Band(1)),
1200                        op: BinaryOp::Multiply,
1201                        right: Box::new(Expr::Band(2)),
1202                        ty: Type::Raster,
1203                    }),
1204                },
1205                Statement::Expr(Box::new(Expr::Variable("used".to_string()))),
1206            ],
1207        };
1208
1209        let opt = Optimizer::new(OptLevel::Aggressive);
1210        let result = opt.optimize_program(program);
1211
1212        // `dead` must be gone; `used` and the final expr must remain.
1213        assert_eq!(result.statements.len(), 2);
1214        assert!(matches!(
1215            &result.statements[0],
1216            Statement::VariableDecl { name, .. } if name == "used"
1217        ));
1218        assert!(matches!(&result.statements[1], Statement::Expr(_)));
1219    }
1220
1221    #[test]
1222    fn test_dce_keeps_transitively_used_declaration() {
1223        // let a = B1 + B2;
1224        // let b = a * B3;   // b uses a
1225        // b;                // observable uses b -> both a and b are live
1226        let program = Program {
1227            statements: vec![
1228                Statement::VariableDecl {
1229                    name: "a".to_string(),
1230                    value: Box::new(add_bands(1, 2)),
1231                },
1232                Statement::VariableDecl {
1233                    name: "b".to_string(),
1234                    value: Box::new(Expr::Binary {
1235                        left: Box::new(Expr::Variable("a".to_string())),
1236                        op: BinaryOp::Multiply,
1237                        right: Box::new(Expr::Band(3)),
1238                        ty: Type::Raster,
1239                    }),
1240                },
1241                Statement::Expr(Box::new(Expr::Variable("b".to_string()))),
1242            ],
1243        };
1244
1245        let opt = Optimizer::new(OptLevel::Aggressive);
1246        let result = opt.optimize_program(program);
1247
1248        // Nothing dead: all three statements survive.
1249        assert_eq!(result.statements.len(), 3);
1250    }
1251
1252    #[test]
1253    fn test_dce_not_applied_below_aggressive() {
1254        let program = Program {
1255            statements: vec![
1256                Statement::VariableDecl {
1257                    name: "dead".to_string(),
1258                    value: Box::new(add_bands(1, 2)),
1259                },
1260                Statement::Expr(Box::new(Expr::Band(1))),
1261            ],
1262        };
1263
1264        let opt = Optimizer::new(OptLevel::Standard);
1265        let result = opt.optimize_program(program);
1266
1267        // Standard level must not strip the (dead) declaration.
1268        assert_eq!(result.statements.len(), 2);
1269    }
1270}