joule_hir/
energy.rs

1//! HIR-level energy budget representation
2//!
3//! This module provides the HIR representation of energy attributes that were
4//! parsed from `#[energy_budget(...)]` attributes and converted from AST.
5//!
6//! The HIR energy types include compile-time estimation results that are
7//! computed during type checking.
8
9use joule_common::Span;
10
11/// HIR-level energy budget attached to functions.
12///
13/// This is the HIR equivalent of `joule_ast::EnergyBudget`, extended with
14/// compile-time checking options and resolved during HIR lowering.
15///
16/// # Example
17/// ```joule
18/// #[energy_budget(max_joules = 0.5, compile_time_check = true)]
19/// fn process(data: &[f32]) -> f32 {
20///     // Compiler will warn if estimated energy exceeds 0.5J
21/// }
22/// ```
23#[derive(Debug, Clone, PartialEq)]
24pub struct HirEnergyBudget {
25    /// Maximum energy consumption in joules (J)
26    pub max_joules: Option<f64>,
27    /// Maximum power consumption in watts (W)
28    pub max_watts: Option<f64>,
29    /// Maximum temperature delta in Celsius
30    pub max_temp_delta: Option<f64>,
31    /// Whether to perform compile-time energy checking
32    /// When true, the compiler will estimate energy and warn/error if exceeded
33    pub compile_time_check: bool,
34    /// Source span for error reporting
35    pub span: Span,
36}
37
38impl HirEnergyBudget {
39    /// Create a new empty energy budget
40    pub const fn new(span: Span) -> Self {
41        Self {
42            max_joules: None,
43            max_watts: None,
44            max_temp_delta: None,
45            compile_time_check: true, // Enable by default
46            span,
47        }
48    }
49
50    /// Create from an AST energy budget
51    pub const fn from_ast(ast_budget: &joule_ast::EnergyBudget) -> Self {
52        Self {
53            max_joules: ast_budget.max_joules,
54            max_watts: ast_budget.max_watts,
55            max_temp_delta: ast_budget.max_temp_delta,
56            compile_time_check: true,
57            span: ast_budget.span,
58        }
59    }
60
61    /// Check if this energy budget has any constraints
62    pub const fn has_constraints(&self) -> bool {
63        self.max_joules.is_some() || self.max_watts.is_some() || self.max_temp_delta.is_some()
64    }
65
66    /// Check if the budget has a joule constraint
67    pub const fn has_joule_constraint(&self) -> bool {
68        self.max_joules.is_some()
69    }
70
71    /// Check if compile-time checking should be performed
72    pub fn should_check(&self) -> bool {
73        self.compile_time_check && self.has_constraints()
74    }
75}
76
77impl Default for HirEnergyBudget {
78    fn default() -> Self {
79        Self {
80            max_joules: None,
81            max_watts: None,
82            max_temp_delta: None,
83            compile_time_check: true,
84            span: Span::dummy(),
85        }
86    }
87}
88
89/// Energy metadata for HIR items (functions, loops, blocks).
90///
91/// This struct holds both the declared budget (if any) and the
92/// compile-time estimated cost computed by the energy estimator.
93#[derive(Debug, Clone, PartialEq, Default)]
94pub struct EnergyMetadata {
95    /// The declared energy budget from `#[energy_budget(...)]` attribute
96    pub budget: Option<HirEnergyBudget>,
97    /// Compile-time estimated energy cost in joules
98    pub estimated_cost: Option<f64>,
99    /// Estimated power consumption in watts (if determinable)
100    pub estimated_power: Option<f64>,
101    /// Estimated temperature impact in Celsius
102    pub estimated_temp_delta: Option<f64>,
103    /// Confidence level of the estimate (0.0 to 1.0)
104    /// Lower values indicate more uncertainty (e.g., due to loops with unknown bounds)
105    pub confidence: f64,
106}
107
108impl EnergyMetadata {
109    /// Create new metadata with just a budget
110    pub const fn with_budget(budget: HirEnergyBudget) -> Self {
111        Self {
112            budget: Some(budget),
113            estimated_cost: None,
114            estimated_power: None,
115            estimated_temp_delta: None,
116            confidence: 0.0,
117        }
118    }
119
120    /// Create new metadata with an estimated cost
121    pub const fn with_estimate(estimated_cost: f64, confidence: f64) -> Self {
122        Self {
123            budget: None,
124            estimated_cost: Some(estimated_cost),
125            estimated_power: None,
126            estimated_temp_delta: None,
127            confidence,
128        }
129    }
130
131    /// Check if the estimated cost exceeds the budget
132    pub fn exceeds_budget(&self) -> bool {
133        if let (Some(budget), Some(estimated)) = (&self.budget, self.estimated_cost)
134            && let Some(max_joules) = budget.max_joules
135        {
136            return estimated > max_joules;
137        }
138        false
139    }
140
141    /// Get the amount by which the estimate exceeds the budget (if any)
142    pub fn budget_excess(&self) -> Option<f64> {
143        if let (Some(budget), Some(estimated)) = (&self.budget, self.estimated_cost)
144            && let Some(max_joules) = budget.max_joules
145        {
146            let excess = estimated - max_joules;
147            if excess > 0.0 {
148                return Some(excess);
149            }
150        }
151        None
152    }
153
154    /// Check if the power estimate exceeds the budget
155    pub fn exceeds_power_budget(&self) -> bool {
156        if let (Some(budget), Some(estimated)) = (&self.budget, self.estimated_power)
157            && let Some(max_watts) = budget.max_watts
158        {
159            return estimated > max_watts;
160        }
161        false
162    }
163
164    /// Check if the temperature estimate exceeds the budget
165    pub fn exceeds_temp_budget(&self) -> bool {
166        if let (Some(budget), Some(estimated)) = (&self.budget, self.estimated_temp_delta)
167            && let Some(max_temp) = budget.max_temp_delta
168        {
169            return estimated > max_temp;
170        }
171        false
172    }
173
174    /// Set the estimated cost
175    pub const fn set_estimate(&mut self, cost: f64, confidence: f64) {
176        self.estimated_cost = Some(cost);
177        self.confidence = confidence;
178    }
179
180    /// Check if we have any estimate
181    pub const fn has_estimate(&self) -> bool {
182        self.estimated_cost.is_some()
183    }
184}
185
186/// Energy violation detected during compile-time analysis.
187///
188/// This is returned when energy estimation determines that a function's
189/// estimated energy consumption exceeds its declared budget.
190#[derive(Debug, Clone, PartialEq)]
191pub struct EnergyViolation {
192    /// The kind of violation
193    pub kind: EnergyViolationKind,
194    /// The declared budget value
195    pub budget: f64,
196    /// The estimated actual value
197    pub estimated: f64,
198    /// Source span where the budget was declared
199    pub budget_span: Span,
200    /// Source span of the violating code (e.g., function body)
201    pub code_span: Span,
202    /// Confidence level of the estimate (0.0 to 1.0)
203    pub confidence: f64,
204    /// Human-readable description of the violation
205    pub message: String,
206    /// Callee chain showing where the energy went: (callee_name, cost_joules)
207    /// Populated only for transitive budget violations
208    pub callee_chain: Vec<(String, f64)>,
209}
210
211impl EnergyViolation {
212    /// Format a joule value with appropriate SI prefix for readability.
213    /// Values >= 0.001J use plain joules; smaller values use mJ/uJ/nJ/pJ.
214    pub fn format_joules(joules: f64) -> String {
215        let abs = joules.abs();
216        if abs >= 0.001 {
217            format!("{joules:.3}J")
218        } else if abs >= 1e-6 {
219            format!("{:.3}mJ", joules * 1e3)
220        } else if abs >= 1e-9 {
221            format!("{:.3}uJ", joules * 1e6)
222        } else if abs >= 1e-12 {
223            format!("{:.3}nJ", joules * 1e9)
224        } else {
225            format!("{:.3}pJ", joules * 1e12)
226        }
227    }
228
229    /// Create a new joule budget violation
230    pub fn joules(
231        budget: f64,
232        estimated: f64,
233        budget_span: Span,
234        code_span: Span,
235        confidence: f64,
236    ) -> Self {
237        let est_str = Self::format_joules(estimated);
238        let bud_str = Self::format_joules(budget);
239        Self {
240            kind: EnergyViolationKind::JoulesExceeded,
241            budget,
242            estimated,
243            budget_span,
244            code_span,
245            confidence,
246            message: format!("estimated energy consumption {est_str} exceeds budget of {bud_str}"),
247            callee_chain: Vec::new(),
248        }
249    }
250
251    /// Create a new power budget violation
252    pub fn watts(
253        budget: f64,
254        estimated: f64,
255        budget_span: Span,
256        code_span: Span,
257        confidence: f64,
258    ) -> Self {
259        Self {
260            kind: EnergyViolationKind::WattsExceeded,
261            budget,
262            estimated,
263            budget_span,
264            code_span,
265            confidence,
266            message: format!(
267                "estimated power consumption {estimated:.1}W exceeds budget of {budget:.1}W"
268            ),
269            callee_chain: Vec::new(),
270        }
271    }
272
273    /// Create a new temperature budget violation
274    pub fn temp_delta(
275        budget: f64,
276        estimated: f64,
277        budget_span: Span,
278        code_span: Span,
279        confidence: f64,
280    ) -> Self {
281        Self {
282            kind: EnergyViolationKind::TempDeltaExceeded,
283            budget,
284            estimated,
285            budget_span,
286            code_span,
287            confidence,
288            message: format!(
289                "estimated temperature increase {estimated:.1}C exceeds budget of {budget:.1}C"
290            ),
291            callee_chain: Vec::new(),
292        }
293    }
294
295    /// Get the percentage by which the estimate exceeds the budget
296    pub fn excess_percentage(&self) -> f64 {
297        ((self.estimated - self.budget) / self.budget) * 100.0
298    }
299}
300
301/// Kind of energy budget violation
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
303pub enum EnergyViolationKind {
304    /// Energy consumption (joules) exceeded
305    JoulesExceeded,
306    /// Power consumption (watts) exceeded
307    WattsExceeded,
308    /// Temperature delta exceeded
309    TempDeltaExceeded,
310}
311
312impl std::fmt::Display for EnergyViolationKind {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        match self {
315            Self::JoulesExceeded => write!(f, "energy budget exceeded"),
316            Self::WattsExceeded => write!(f, "power budget exceeded"),
317            Self::TempDeltaExceeded => write!(f, "temperature budget exceeded"),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_hir_energy_budget_default() {
328        let budget = HirEnergyBudget::default();
329        assert!(budget.max_joules.is_none());
330        assert!(budget.compile_time_check);
331        assert!(!budget.has_constraints());
332    }
333
334    #[test]
335    fn test_hir_energy_budget_from_ast() {
336        let mut ast_budget = joule_ast::EnergyBudget::new(Span::dummy());
337        ast_budget.set_max_joules(0.5);
338        ast_budget.set_max_watts(25.0);
339
340        let hir_budget = HirEnergyBudget::from_ast(&ast_budget);
341        assert_eq!(hir_budget.max_joules, Some(0.5));
342        assert_eq!(hir_budget.max_watts, Some(25.0));
343        assert!(hir_budget.has_constraints());
344        assert!(hir_budget.should_check());
345    }
346
347    #[test]
348    fn test_energy_metadata_exceeds_budget() {
349        let mut budget = HirEnergyBudget::new(Span::dummy());
350        budget.max_joules = Some(0.5);
351
352        let mut metadata = EnergyMetadata::with_budget(budget);
353        metadata.estimated_cost = Some(0.8);
354
355        assert!(metadata.exceeds_budget());
356        // Use approximate comparison for floating-point
357        let excess = metadata.budget_excess().unwrap();
358        assert!(
359            (excess - 0.3).abs() < 1e-10,
360            "Expected ~0.3, got {}",
361            excess
362        );
363    }
364
365    #[test]
366    fn test_energy_metadata_within_budget() {
367        let mut budget = HirEnergyBudget::new(Span::dummy());
368        budget.max_joules = Some(1.0);
369
370        let mut metadata = EnergyMetadata::with_budget(budget);
371        metadata.estimated_cost = Some(0.5);
372
373        assert!(!metadata.exceeds_budget());
374        assert!(metadata.budget_excess().is_none());
375    }
376
377    #[test]
378    fn test_energy_violation() {
379        let violation = EnergyViolation::joules(0.5, 0.8, Span::dummy(), Span::dummy(), 0.9);
380
381        assert_eq!(violation.kind, EnergyViolationKind::JoulesExceeded);
382        assert!((violation.excess_percentage() - 60.0).abs() < 0.01);
383        assert!(violation.message.contains("0.800J"));
384        assert!(violation.message.contains("0.500J"));
385    }
386}