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 orchestrator. Walks input, calls parser + emit, writes outputs,
//! builds a report.

use std::path::PathBuf;

use anyhow::Result;
use tracing::{info, warn};

use crate::cli::Target;
use crate::report::{FileReport, MigrationReport, ReportSeverity};
use crate::targets;
use crate::walker;

#[derive(Debug, Clone)]
pub struct MigrateOptions {
    pub input: PathBuf,
    pub output: PathBuf,
    pub target: Target,
    pub dry_run: bool,
    pub verbose: bool,
    pub filter: Option<String>,
}

pub fn run(opts: MigrateOptions) -> Result<MigrationReport> {
    let files = walker::collect_cuda_files(&opts.input, opts.filter.as_deref())?;
    if files.is_empty() {
        anyhow::bail!(
            "no .cu/.cuh files found at {}",
            opts.input.display()
        );
    }
    info!("found {} CUDA file(s)", files.len());

    let targets_to_run: Vec<Target> = match opts.target {
        Target::All => Target::iter_real().to_vec(),
        t => vec![t],
    };

    let target_dirs: Vec<PathBuf> = targets_to_run
        .iter()
        .map(|t| opts.output.join(t.as_str()))
        .collect();

    if !opts.dry_run {
        for d in &target_dirs {
            std::fs::create_dir_all(d)?;
        }
    }

    let mut report = MigrationReport::new(opts.input.clone(), opts.output.clone());

    for file in &files {
        if opts.verbose {
            info!("parsing {}", file.display());
        }
        let unit = match crate::parser::translate_path(file) {
            Ok(u) => u,
            Err(e) => {
                warn!("parse error in {}: {e}", file.display());
                report.push(FileReport {
                    source: file.clone(),
                    outputs: vec![],
                    warnings: vec![(ReportSeverity::Error, format!("parse error: {e}"))],
                });
                continue;
            }
        };

        let mut outputs = Vec::new();
        for t in &targets_to_run {
            let backend = targets::for_target(*t);
            let text = backend.emit(&unit);
            let out = walker::map_output_path(
                &root_dir(&opts.input),
                &opts.output,
                t.as_str(),
                file,
            );
            let warnings = collect_warnings(&unit, *t);
            if !opts.dry_run {
                walker::ensure_parent_dir(&out)?;
                std::fs::write(&out, text)?;
                if opts.verbose {
                    info!("wrote {}", out.display());
                }
            }
            outputs.push((t.as_str().to_string(), out));
            for w in warnings {
                report.record_warning(&unit.path, t.as_str(), w);
            }
        }

        // Per-file diagnostic of unknown call mappings.
        for n in &unit.nodes {
            if let crate::ir::IrNode::RuntimeCall { name, mappings, .. } = n {
                for t in &targets_to_run {
                    if mappings[crate::ir::slot_index(*t)].is_none() {
                        report.record_warning(
                            &unit.path,
                            t.as_str(),
                            format!(
                                "no automatic mapping for CUDA API `{name}`; left as TODO"
                            ),
                        );
                    }
                }
            }
        }

        report.push(FileReport {
            source: file.clone(),
            outputs,
            warnings: Vec::new(),
        });
    }

    report.report_path = opts
        .output
        .join(format!("migration-report.json.{}", report.started_at));

    if !opts.dry_run {
        crate::report::write_json_report(&report, &report.report_path)?;
    }

    Ok(report)
}

fn root_dir(input: &std::path::Path) -> std::path::PathBuf {
    if input.is_dir() {
        input.to_path_buf()
    } else {
        input
            .parent()
            .map(std::path::Path::to_path_buf)
            .unwrap_or_else(|| std::path::PathBuf::from("."))
    }
}

fn collect_warnings(unit: &crate::ir::TranslationUnit, _target: Target) -> Vec<String> {
    let mut out = Vec::new();
    for n in &unit.nodes {
        if let crate::ir::IrNode::Warning { message, .. } = n {
            out.push(message.clone());
        }
        if let crate::ir::IrNode::QualifierDecl { qualifier, .. } = n {
            use crate::ir::CudaQualifier::*;
            match qualifier {
                LaunchBounds | ClusterDim | GridDim => out.push(format!(
                    "qualifier {qualifier:?} is not auto-translated; manual review needed"
                )),
                _ => {}
            }
        }
    }
    out
}