1use joule_common::Span;
10
11#[derive(Debug, Clone, PartialEq)]
24pub struct HirEnergyBudget {
25 pub max_joules: Option<f64>,
27 pub max_watts: Option<f64>,
29 pub max_temp_delta: Option<f64>,
31 pub compile_time_check: bool,
34 pub span: Span,
36}
37
38impl HirEnergyBudget {
39 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, span,
47 }
48 }
49
50 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 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 pub const fn has_joule_constraint(&self) -> bool {
68 self.max_joules.is_some()
69 }
70
71 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#[derive(Debug, Clone, PartialEq, Default)]
94pub struct EnergyMetadata {
95 pub budget: Option<HirEnergyBudget>,
97 pub estimated_cost: Option<f64>,
99 pub estimated_power: Option<f64>,
101 pub estimated_temp_delta: Option<f64>,
103 pub confidence: f64,
106}
107
108impl EnergyMetadata {
109 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 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 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 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 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 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 pub const fn set_estimate(&mut self, cost: f64, confidence: f64) {
176 self.estimated_cost = Some(cost);
177 self.confidence = confidence;
178 }
179
180 pub const fn has_estimate(&self) -> bool {
182 self.estimated_cost.is_some()
183 }
184}
185
186#[derive(Debug, Clone, PartialEq)]
191pub struct EnergyViolation {
192 pub kind: EnergyViolationKind,
194 pub budget: f64,
196 pub estimated: f64,
198 pub budget_span: Span,
200 pub code_span: Span,
202 pub confidence: f64,
204 pub message: String,
206 pub callee_chain: Vec<(String, f64)>,
209}
210
211impl EnergyViolation {
212 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 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 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 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 pub fn excess_percentage(&self) -> f64 {
297 ((self.estimated - self.budget) / self.budget) * 100.0
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
303pub enum EnergyViolationKind {
304 JoulesExceeded,
306 WattsExceeded,
308 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 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}