vyre 0.4.0

GPU compute intermediate representation with a standard operation library
Documentation
use std::fs::{self, File, Metadata};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

const ROOT_EXPORT_PREFIX: &str = "//! @root_export ";
const SOURCE_HEADER_LINES: usize = 20;

pub(crate) struct ModuleLeaf {
    absolute_path: PathBuf,
    exports: Vec<String>,
    hash: u64,
}

pub(crate) fn discover_module_leaves(src_root: &Path) -> Result<Vec<ModuleLeaf>, String> {
    let metadata = super::checked_metadata(src_root)?;
    if !metadata.is_dir() {
        return Err(format!(
            "Fix: make source root a real directory at {}",
            src_root.display()
        ));
    }
    let mut leaves = Vec::new();
    walk_source_tree(src_root, src_root, &mut leaves)?;
    leaves.sort_by(|left, right| left.absolute_path.cmp(&right.absolute_path));
    Ok(leaves)
}

fn walk_source_tree(
    src_root: &Path,
    dir: &Path,
    leaves: &mut Vec<ModuleLeaf>,
) -> Result<(), String> {
    println!("cargo:rerun-if-changed={}", dir.display());
    let mut entries = fs::read_dir(dir)
        .map_err(|error| {
            format!(
                "Fix: make source directory readable at {}: {error}",
                dir.display()
            )
        })?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|error| {
            format!(
                "Fix: make every source entry under {} readable: {error}",
                dir.display()
            )
        })?;
    entries.sort_by_key(|entry| entry.path());

    for entry in entries {
        let path = entry.path();
        let metadata = super::checked_metadata(&path)?;
        if metadata.is_dir() {
            walk_source_tree(src_root, &path, leaves)?;
        } else if is_module_leaf(&path, &metadata) {
            let relative = path.strip_prefix(src_root).map_err(|error| {
                format!(
                    "Fix: keep source file {} inside source root {}: {error}",
                    path.display(),
                    src_root.display()
                )
            })?;
            leaves.push(ModuleLeaf {
                absolute_path: path.canonicalize().map_err(|error| {
                    format!(
                        "Fix: canonicalize Rust source path at {}: {error}",
                        path.display()
                    )
                })?,
                exports: read_root_exports(&path)?,
                hash: stable_path_hash(relative),
            });
        }
    }
    Ok(())
}

fn is_module_leaf(path: &Path, metadata: &Metadata) -> bool {
    metadata.is_file()
        && path.extension().is_some_and(|extension| extension == "rs")
        && !path
            .file_name()
            .is_some_and(|name| name == "lib.rs" || name == "mod.rs")
}

fn read_root_exports(path: &Path) -> Result<Vec<String>, String> {
    let file = File::open(path).map_err(|error| {
        format!(
            "Fix: make Rust source readable at {}: {error}",
            path.display()
        )
    })?;
    let mut exports = Vec::new();
    for (index, line) in BufReader::new(file)
        .lines()
        .take(SOURCE_HEADER_LINES)
        .enumerate()
    {
        let line = line.map_err(|error| {
            format!(
                "Fix: make line {} readable in {}: {error}",
                index + 1,
                path.display()
            )
        })?;
        if let Some(export) = parse_root_export(&line, path, index + 1)? {
            exports.push(export);
        }
    }
    Ok(exports)
}

fn parse_root_export(
    line: &str,
    path: &Path,
    line_number: usize,
) -> Result<Option<String>, String> {
    let Some(raw_name) = line.strip_prefix(ROOT_EXPORT_PREFIX) else {
        return Ok(None);
    };
    let name = raw_name.trim();
    if is_rust_identifier(name) {
        Ok(Some(name.to_owned()))
    } else {
        Err(format!(
            "Fix: change @root_export directive at {}:{line_number} to one Rust identifier",
            path.display()
        ))
    }
}

fn is_rust_identifier(name: &str) -> bool {
    let mut bytes = name.bytes();
    let Some(first) = bytes.next() else {
        return false;
    };
    matches!(first, b'A'..=b'Z' | b'a'..=b'z' | b'_')
        && bytes.all(|byte| matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_'))
}

fn stable_path_hash(path: &Path) -> u64 {
    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
    for byte in path.to_string_lossy().replace('\\', "/").bytes() {
        hash ^= u64::from(byte);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

pub(crate) fn render_module_tree(leaves: &[ModuleLeaf]) -> String {
    let mut output = String::from(
        "// Generated source module enumeration for zero-conflict migration.\n\
         // Not included by lib.rs until the migration commit activates it.\n",
    );
    for leaf in leaves {
        output.push_str(&format!(
            "#[path = {path:?}] mod __vyre_auto_{hash:016x};\n",
            path = leaf.absolute_path.display().to_string(),
            hash = leaf.hash
        ));
    }
    for leaf in leaves {
        for export in &leaf.exports {
            output.push_str(&format!(
                "pub use __vyre_auto_{hash:016x}::{export} as {export};\n",
                hash = leaf.hash
            ));
        }
    }
    output
}