eclexiaiser 0.1.0

Add energy, carbon, and resource-cost awareness to existing software via Eclexia economics-as-code
Documentation
// SPDX-License-Identifier: PMPL-1.0-or-later
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// Instrumenter submodule — generates energy measurement instrumentation code
// and Eclexia constraint definitions from validated function budgets.
//
// The generated code wraps target functions with energy/time measurement hooks
// that record per-call resource consumption. The Eclexia constraint file defines
// the formal energy/carbon bounds that the runtime must enforce.

use anyhow::Result;

use crate::codegen::parser::ParsedFunctionBudget;
use crate::manifest::Manifest;

/// Generate Rust instrumentation wrapper code for all budgeted functions.
///
/// The generated code provides:
/// - A `measure_energy` helper that uses `std::time::Instant` as a proxy for
///   energy (wall-clock time * estimated power draw). In Phase 2, this will
///   integrate with RAPL, perf_events, or external power meters.
/// - Per-function wrapper structs that record measurements and check budgets.
/// - A `collect_measurements` function that aggregates all results.
///
/// # Arguments
/// - `manifest` — The validated manifest for project metadata.
/// - `functions` — Parsed and validated function budgets.
///
/// # Returns
/// A string containing the generated Rust source code.
pub fn generate_instrumentation(
    manifest: &Manifest,
    functions: &[ParsedFunctionBudget],
) -> Result<String> {
    let mut code = String::with_capacity(4096);

    // File header.
    code.push_str(&format!(
        "// Auto-generated by eclexiaiser for project '{}'\n",
        manifest.project.name
    ));
    code.push_str("// SPDX-License-Identifier: PMPL-1.0-or-later\n");
    code.push_str("//\n");
    code.push_str("// Energy/carbon instrumentation wrappers.\n");
    code.push_str("// DO NOT EDIT — regenerate with `eclexiaiser generate`.\n\n");
    code.push_str("use std::time::Instant;\n\n");

    // Estimated power draw constant (milliwatts). In Phase 2, this will be
    // measured dynamically or read from hardware counters.
    code.push_str("/// Estimated average power draw in milliwatts.\n");
    code.push_str("/// Replace with actual measurement in production.\n");
    code.push_str("const ESTIMATED_POWER_MW: f64 = 15000.0; // 15W typical\n\n");

    // Carbon intensity from manifest.
    code.push_str(&format!(
        "/// Carbon intensity: {} mg CO2/kWh (provider: {}, region: {})\n",
        manifest.carbon.static_intensity,
        manifest.carbon.provider.display_name(),
        manifest.carbon.region
    ));
    code.push_str(&format!(
        "const CARBON_INTENSITY_MG_PER_KWH: f64 = {:.1};\n\n",
        manifest.carbon.static_intensity
    ));

    // Energy measurement helper.
    code.push_str("/// Measure energy consumption of a closure in millijoules.\n");
    code.push_str("///\n");
    code.push_str("/// Uses wall-clock time * estimated power draw as a proxy.\n");
    code.push_str("/// Returns (result, energy_mj, duration_ms).\n");
    code.push_str("pub fn measure_energy<F, R>(f: F) -> (R, f64, f64)\n");
    code.push_str("where\n");
    code.push_str("    F: FnOnce() -> R,\n");
    code.push_str("{\n");
    code.push_str("    let start = Instant::now();\n");
    code.push_str("    let result = f();\n");
    code.push_str("    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;\n");
    code.push_str("    // energy (mJ) = power (mW) * time (s) = power (mW) * time_ms / 1000\n");
    code.push_str("    let energy_mj = ESTIMATED_POWER_MW * elapsed_ms / 1000.0;\n");
    code.push_str("    (result, energy_mj, elapsed_ms)\n");
    code.push_str("}\n\n");

    // Carbon estimation helper.
    code.push_str("/// Estimate carbon emissions from energy consumption.\n");
    code.push_str("///\n");
    code.push_str("/// Returns mg CO2 from millijoules of energy.\n");
    code.push_str("pub fn estimate_carbon_mg(energy_mj: f64) -> f64 {\n");
    code.push_str("    energy_mj * CARBON_INTENSITY_MG_PER_KWH / 3_600_000_000.0\n");
    code.push_str("}\n\n");

    // Per-function budget check structs.
    code.push_str("/// Measurement result for a single function call.\n");
    code.push_str("#[derive(Debug)]\n");
    code.push_str("pub struct Measurement {\n");
    code.push_str("    pub function_name: &'static str,\n");
    code.push_str("    pub energy_mj: f64,\n");
    code.push_str("    pub carbon_mg: f64,\n");
    code.push_str("    pub duration_ms: f64,\n");
    code.push_str("    pub energy_budget_mj: Option<f64>,\n");
    code.push_str("    pub carbon_budget_mg: Option<f64>,\n");
    code.push_str("    pub energy_exceeded: bool,\n");
    code.push_str("    pub carbon_exceeded: bool,\n");
    code.push_str("}\n\n");

    // Generate per-function measurement wrappers.
    for func in functions {
        generate_function_wrapper(&mut code, func);
    }

    Ok(code)
}

/// Generate a measurement wrapper function for a single budgeted function.
fn generate_function_wrapper(code: &mut String, func: &ParsedFunctionBudget) {
    let safe_name = func.name.replace("::", "_");

    // Budget constants.
    if let Some(ref budget) = func.energy_budget {
        code.push_str(&format!(
            "const {}_ENERGY_BUDGET_MJ: f64 = {:.1};\n",
            safe_name.to_uppercase(),
            budget.max_millijoules
        ));
    }
    if let Some(ref budget) = func.carbon_budget {
        code.push_str(&format!(
            "const {}_CARBON_BUDGET_MG: f64 = {:.1};\n",
            safe_name.to_uppercase(),
            budget.max_mg_co2
        ));
    }

    // Wrapper function.
    code.push_str(&format!(
        "\n/// Energy-instrumented wrapper for `{}`.\n",
        func.name
    ));
    code.push_str(&format!(
        "/// Source: {}\n",
        func.source
    ));
    code.push_str(&format!(
        "pub fn measure_{safe_name}<F, R>(f: F) -> (R, Measurement)\n"
    ));
    code.push_str("where\n");
    code.push_str("    F: FnOnce() -> R,\n");
    code.push_str("{\n");
    code.push_str("    let (result, energy_mj, duration_ms) = measure_energy(f);\n");
    code.push_str("    let carbon_mg = estimate_carbon_mg(energy_mj);\n");

    // Budget checks.
    let energy_budget_expr = if func.energy_budget.is_some() {
        format!("Some({}_ENERGY_BUDGET_MJ)", safe_name.to_uppercase())
    } else {
        "None".to_string()
    };
    let carbon_budget_expr = if func.carbon_budget.is_some() {
        format!("Some({}_CARBON_BUDGET_MG)", safe_name.to_uppercase())
    } else {
        "None".to_string()
    };

    let energy_exceeded = if func.energy_budget.is_some() {
        format!(
            "energy_mj > {}_ENERGY_BUDGET_MJ",
            safe_name.to_uppercase()
        )
    } else {
        "false".to_string()
    };
    let carbon_exceeded = if func.carbon_budget.is_some() {
        format!(
            "carbon_mg > {}_CARBON_BUDGET_MG",
            safe_name.to_uppercase()
        )
    } else {
        "false".to_string()
    };

    code.push_str(&format!(
        "    let measurement = Measurement {{\n\
         \x20       function_name: \"{}\",\n\
         \x20       energy_mj,\n\
         \x20       carbon_mg,\n\
         \x20       duration_ms,\n\
         \x20       energy_budget_mj: {energy_budget_expr},\n\
         \x20       carbon_budget_mg: {carbon_budget_expr},\n\
         \x20       energy_exceeded: {energy_exceeded},\n\
         \x20       carbon_exceeded: {carbon_exceeded},\n\
         \x20   }};\n",
        func.name
    ));
    code.push_str("    (result, measurement)\n");
    code.push_str("}\n\n");
}

/// Generate Eclexia constraint definitions (.ecl file) for all budgeted functions.
///
/// The constraint file defines formal energy/carbon bounds in Eclexia's
/// economics-as-code format. These constraints can be loaded by the Eclexia
/// runtime to enforce sustainability limits at execution time.
pub fn generate_constraints(
    manifest: &Manifest,
    functions: &[ParsedFunctionBudget],
) -> Result<String> {
    let mut ecl = String::with_capacity(2048);

    // Header.
    ecl.push_str(&format!(
        ";; Eclexia energy constraints for '{}'\n",
        manifest.project.name
    ));
    ecl.push_str(";; Auto-generated by eclexiaiser — DO NOT EDIT\n");
    ecl.push_str(";; SPDX-License-Identifier: PMPL-1.0-or-later\n\n");

    // Global carbon configuration.
    ecl.push_str("(carbon-config\n");
    ecl.push_str(&format!(
        "  (provider \"{}\")\n",
        manifest.carbon.provider.display_name().to_lowercase()
    ));
    ecl.push_str(&format!("  (region \"{}\")\n", manifest.carbon.region));
    ecl.push_str(&format!(
        "  (intensity-mg-per-kwh {:.1}))\n\n",
        manifest.carbon.static_intensity
    ));

    // Per-function constraints.
    for func in functions {
        ecl.push_str(&format!(";; Constraints for {}\n", func.name));
        ecl.push_str(&format!("(define-constraint {}\n", func.name));
        ecl.push_str(&format!("  (source \"{}\")\n", func.source));

        if let Some(ref budget) = func.energy_budget {
            ecl.push_str(&format!(
                "  (energy-bound-mj {:.1})\n",
                budget.max_millijoules
            ));
        }
        if let Some(ref budget) = func.carbon_budget {
            ecl.push_str(&format!(
                "  (carbon-bound-mg {:.1})\n",
                budget.max_mg_co2
            ));
        }

        ecl.push_str("  (enforcement strict))\n\n");
    }

    Ok(ecl)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::abi::{
        CarbonConfig, CarbonProvider, FunctionBudget, ReportConfig, ReportFormat,
    };
    use crate::codegen::parser::parse_function_budgets;
    use crate::manifest::{Manifest, ProjectConfig};

    fn test_manifest() -> Manifest {
        Manifest {
            project: ProjectConfig {
                name: "test-project".to_string(),
            },
            functions: vec![
                FunctionBudget {
                    name: "process_batch".to_string(),
                    source: "src/batch.rs".to_string(),
                    energy_budget_mj: Some(50.0),
                    carbon_budget_mg: Some(10.0),
                },
                FunctionBudget {
                    name: "render_page".to_string(),
                    source: "src/web.rs".to_string(),
                    energy_budget_mj: Some(5.0),
                    carbon_budget_mg: None,
                },
            ],
            carbon: CarbonConfig {
                provider: CarbonProvider::Static,
                region: "GB".to_string(),
                static_intensity: 200.0,
            },
            report: ReportConfig {
                format: ReportFormat::Text,
                include_recommendations: true,
            },
        }
    }

    #[test]
    fn test_generate_instrumentation_contains_wrappers() {
        let m = test_manifest();
        let parsed = parse_function_budgets(&m.functions).unwrap();
        let code = generate_instrumentation(&m, &parsed).unwrap();

        assert!(code.contains("measure_process_batch"));
        assert!(code.contains("measure_render_page"));
        assert!(code.contains("PROCESS_BATCH_ENERGY_BUDGET_MJ"));
        assert!(code.contains("RENDER_PAGE_ENERGY_BUDGET_MJ"));
        assert!(code.contains("PROCESS_BATCH_CARBON_BUDGET_MG"));
        // render_page has no carbon budget, so this should not appear.
        assert!(!code.contains("RENDER_PAGE_CARBON_BUDGET_MG"));
    }

    #[test]
    fn test_generate_constraints_format() {
        let m = test_manifest();
        let parsed = parse_function_budgets(&m.functions).unwrap();
        let ecl = generate_constraints(&m, &parsed).unwrap();

        assert!(ecl.contains("(carbon-config"));
        assert!(ecl.contains("(region \"GB\")"));
        assert!(ecl.contains("(define-constraint process_batch"));
        assert!(ecl.contains("(energy-bound-mj 50.0)"));
        assert!(ecl.contains("(carbon-bound-mg 10.0)"));
        assert!(ecl.contains("(define-constraint render_page"));
    }
}