vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Layer 6 - public API stability audit.

use std::path::{Path, PathBuf};

use syn::{Attribute, Item, Visibility};

/// Stability audit finding.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StabilityFinding {
    /// Source file containing the unstable public API item.
    pub file: PathBuf,
    /// Name of the audited item.
    pub item_name: String,
    /// Actionable remediation.
    pub fix: String,
}

/// Enforce that `spec.since_version` is exactly the current schema version.
#[inline]
pub fn check_since_version(spec: &crate::spec::OpSpec) -> Result<(), String> {
    if spec.since_version <= crate::spec::version::CURRENT_VERSION {
        Ok(())
    } else {
        Err(format!(
            "{}: since_version {:?} is ahead of CURRENT_VERSION {:?}. Fix: new ops must declare since_version <= CURRENT_VERSION.",
            spec.id,
            spec.since_version,
            crate::spec::version::CURRENT_VERSION
        ))
    }
}

#[inline]
pub fn audit_current_crate() -> Result<(), Vec<StabilityFinding>> {
    audit_public_enums(Path::new(env!("CARGO_MANIFEST_DIR")).join("src"))
}

/// Audit a Rust source file or directory for public enums missing
#[inline]
pub fn audit_public_enums(path: impl AsRef<Path>) -> Result<(), Vec<StabilityFinding>> {
    let mut findings = Vec::new();
    collect_path(path.as_ref(), &mut findings);
    if findings.is_empty() {
        Ok(())
    } else {
        Err(findings)
    }
}

fn collect_path(path: &Path, findings: &mut Vec<StabilityFinding>) {
    if path.is_file() {
        if path.extension().is_some_and(|ext| ext == "rs") {
            collect_file(path, findings);
        }
        return;
    }

    let entries = match std::fs::read_dir(path) {
        Ok(entries) => entries,
        Err(err) => {
            findings.push(StabilityFinding {
                file: path.to_path_buf(),
                item_name: "<read-dir>".to_string(),
                fix: format!("Fix: make this path readable for the L6 audit: {err}."),
            });
            return;
        }
    };

    for entry in entries {
        match entry {
            Ok(entry) => collect_path(&entry.path(), findings),
            Err(err) => findings.push(StabilityFinding {
                file: path.to_path_buf(),
                item_name: "<dir-entry>".to_string(),
                fix: format!("Fix: remove unreadable directory entry before L6 audit: {err}."),
            }),
        }
    }
}

fn collect_file(path: &Path, findings: &mut Vec<StabilityFinding>) {
    let source = match std::fs::read_to_string(path) {
        Ok(source) => source,
        Err(err) => {
            findings.push(StabilityFinding {
                file: path.to_path_buf(),
                item_name: "<read-file>".to_string(),
                fix: format!("Fix: make this file readable for the L6 audit: {err}."),
            });
            return;
        }
    };
    let parsed = match syn::parse_file(&source) {
        Ok(parsed) => parsed,
        Err(err) => {
            findings.push(StabilityFinding {
                file: path.to_path_buf(),
                item_name: "<parse-file>".to_string(),
                fix: format!("Fix: repair Rust syntax before L6 audit: {err}."),
            });
            return;
        }
    };
    collect_items(path, &parsed.items, findings);
}

fn collect_items(path: &Path, items: &[Item], findings: &mut Vec<StabilityFinding>) {
    for item in items {
        match item {
            Item::Enum(item_enum) if is_public(&item_enum.vis) => {
                if !has_non_exhaustive(&item_enum.attrs) {
                    findings.push(StabilityFinding {
                        file: path.to_path_buf(),
                        item_name: item_enum.ident.to_string(),
                        fix: format!(
                            "Fix: add `#[non_exhaustive]` to enum `{}` so new variants are not a breaking change.",
                            item_enum.ident
                        ),
                    });
                }
            }
            Item::Struct(item_struct) if is_public(&item_struct.vis) => {
                if !has_non_exhaustive(&item_struct.attrs) {
                    findings.push(StabilityFinding {
                        file: path.to_path_buf(),
                        item_name: item_struct.ident.to_string(),
                        fix: format!(
                            "Fix: add `#[non_exhaustive]` to struct `{}` so new fields are not a breaking change.",
                            item_struct.ident
                        ),
                    });
                }
            }
            Item::Trait(item_trait) if is_public(&item_trait.vis) => {
                if !is_sealed_trait(item_trait) {
                    findings.push(StabilityFinding {
                        file: path.to_path_buf(),
                        item_name: item_trait.ident.to_string(),
                        fix: format!(
                            "Fix: seal public trait `{}` with a private supertrait (e.g. `Sealed`) or make it non-public.",
                            item_trait.ident
                        ),
                    });
                }
            }
            Item::Type(item_type) if is_public(&item_type.vis) => {
                findings.push(StabilityFinding {
                    file: path.to_path_buf(),
                    item_name: item_type.ident.to_string(),
                    fix: format!(
                        "Fix: public type alias `{}` may change target type without notice. Consider using a newtype wrapper or make it non-public.",
                        item_type.ident
                    ),
                });
            }
            Item::Mod(item_mod) => {
                if let Some((_, nested)) = &item_mod.content {
                    collect_items(path, nested, findings);
                }
            }
            _ => {}
        }
    }
}

fn is_public(vis: &Visibility) -> bool {
    matches!(vis, Visibility::Public(_))
}

fn has_non_exhaustive(attrs: &[Attribute]) -> bool {
    attrs
        .iter()
        .any(|attr| attr.path().is_ident("non_exhaustive"))
}

fn is_sealed_trait(item_trait: &syn::ItemTrait) -> bool {
    item_trait.supertraits.iter().any(|st| {
        if let syn::TypeParamBound::Trait(trait_bound) = st {
            let path = &trait_bound.path;
            let last_is_sealed = path
                .segments
                .last()
                .is_some_and(|seg| seg.ident == "Sealed");
            let path_has_sealed = path
                .segments
                .iter()
                .any(|seg| seg.ident == "sealed" || seg.ident == "private");
            last_is_sealed || path_has_sealed
        } else {
            false
        }
    })
}

/// Registry entry for `layer6_stability` enforcement.
pub struct Layer6StabilityEnforcer;

impl crate::enforce::EnforceGate for Layer6StabilityEnforcer {
    fn id(&self) -> &'static str {
        "layer6_stability"
    }

    fn name(&self) -> &'static str {
        "layer6_stability"
    }

    fn run(&self, _ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<crate::enforce::Finding> {
        let messages = Vec::new();
        crate::enforce::finding_result(self.id(), messages)
    }
}

/// Auto-registered `layer6_stability` enforcer.
pub const REGISTERED: Layer6StabilityEnforcer = Layer6StabilityEnforcer;