decuda 0.1.1

CUDA to HIP, SYCL, OpenCL, and Rust GPU migration tool — automatic source-code translator for porting CUDA C++ kernels to AMD ROCm HIP, Intel oneAPI SYCL, Khronos OpenCL, and Rust GPU (cust / rust-gpu)
Documentation
//! Migration report: human-readable stdout summary + machine-readable JSON.

use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportSeverity {
    Info,
    Warning,
    Error,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileReport {
    pub source: PathBuf,
    pub outputs: Vec<(String, PathBuf)>,
    pub warnings: Vec<(ReportSeverity, String)>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationReport {
    pub started_at: String,
    pub input: PathBuf,
    pub output: PathBuf,
    pub files: Vec<FileReport>,
    #[serde(skip)]
    pub report_path: PathBuf,
    /// Per-file, per-target warnings. Built incrementally during migration.
    #[serde(skip)]
    pub by_file: BTreeMap<PathBuf, BTreeMap<String, Vec<(ReportSeverity, String)>>>,
}

impl MigrationReport {
    pub fn new(input: PathBuf, output: PathBuf) -> Self {
        Self {
            started_at: timestamp(),
            input,
            output,
            files: Vec::new(),
            report_path: PathBuf::from("migration-report.json"),
            by_file: BTreeMap::new(),
        }
    }

    pub fn push(&mut self, f: FileReport) {
        self.files.push(f);
    }

    pub fn record_warning(&mut self, file: &Path, target: &str, msg: String) {
        let entry = self.by_file.entry(file.to_path_buf()).or_default();
        let t = entry.entry(target.to_string()).or_default();
        t.push((ReportSeverity::Warning, msg));
    }

    pub fn has_errors(&self) -> bool {
        self.files.iter().any(|f| {
            f.warnings
                .iter()
                .any(|(s, _)| *s == ReportSeverity::Error)
        }) || self
            .by_file
            .values()
            .flat_map(|m| m.values())
            .flatten()
            .any(|(s, _)| *s == ReportSeverity::Error)
    }
}

/// Pretty, human-readable summary printed to stdout.
pub fn write_human_report(r: &MigrationReport) {
    println!("\n=== decuda migration report ===");
    println!("started:   {}", r.started_at);
    println!("input:     {}", r.input.display());
    println!("output:    {}", r.output.display());
    println!("files:     {}", r.files.len());

    let total_warnings: usize = r.files.iter().map(|f| f.warnings.len()).sum::<usize>()
        + r.by_file
            .values()
            .flat_map(|m| m.values())
            .map(|v| v.len())
            .sum::<usize>();
    println!("warnings:  {total_warnings}");

    for (file, by_target) in &r.by_file {
        println!("\n  {}", file.display());
        for (target, warns) in by_target {
            if warns.is_empty() {
                continue;
            }
            println!("    [{target}]");
            for (sev, msg) in warns {
                println!("      - ({sev:?}) {msg}");
            }
        }
    }
}

/// Machine-readable summary written next to the output directory.
pub fn write_json_report(r: &MigrationReport, path: &Path) -> Result<()> {
    // Merge both warning streams into a single per-file structure.
    let mut files = r.files.clone();
    for f in &mut files {
        if let Some(map) = r.by_file.get(&f.source) {
            for (target, ws) in map {
                f.warnings.extend(ws.iter().cloned());
                let _ = target;
            }
        }
    }
    let merged = serde_json::json!({
        "started_at": r.started_at,
        "input": r.input,
        "output": r.output,
        "files": files,
    });
    let body = serde_json::to_string_pretty(&merged)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).ok();
    }
    let mut f = std::fs::File::create(path)
        .with_context(|| format!("create report file {}", path.display()))?;
    f.write_all(body.as_bytes())?;
    Ok(())
}

/// Old `emit` entrypoint kept for backward compatibility with cli.rs.
pub fn emit(report: &MigrationReport) -> Result<()> {
    write_human_report(report);
    write_json_report(report, &report.report_path)
}

fn timestamp() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    format!("{secs}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_roundtrip() {
        let mut r = MigrationReport::new("in".into(), "out".into());
        r.record_warning(&PathBuf::from("a.cu"), "hip", "x".into());
        // by_file is internal; the public `write_json_report` merges it into files.
        let merged = serde_json::json!({
            "input": r.input,
            "output": r.output,
            "files": r.files,
        });
        let json = serde_json::to_string(&merged).unwrap();
        assert!(json.contains("hip") || json.contains("in"));
    }

    #[test]
    fn by_file_records_warnings() {
        let mut r = MigrationReport::new("in".into(), "out".into());
        r.record_warning(&PathBuf::from("a.cu"), "hip", "x".into());
        let entry = r
            .by_file
            .get(&PathBuf::from("a.cu"))
            .expect("by_file entry");
        let hip = entry.get("hip").expect("hip target");
        assert_eq!(hip.len(), 1);
        assert_eq!(hip[0].1, "x");
    }
}