vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Category enforcement finding data.

use std::fmt;
use std::path::Path;

/// Optional source location for an enforcement finding.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FindingLocation {
    /// Source file containing the finding.
    pub file: String,
    /// One-based source line.
    pub line: usize,
}

impl FindingLocation {
    /// Build a source location from a path and one-based line.
    ///
    /// The path is converted to a display string so that findings are
    /// self-contained and do not borrow from the filesystem walk. This
    /// makes them cheap to clone into reports and serialize to CI logs.
    #[inline]
    pub fn new(path: &Path, line: usize) -> Self {
        Self {
            file: path.display().to_string(),
            line,
        }
    }
}

/// Category gate failure with an actionable remediation.
///
/// The category gate enforces the A/B/C taxonomy: Category A ops must be
/// zero-overhead compositions, Category B patterns are forbidden entirely,
/// and Category C ops must declare honest per-backend availability. Every
/// variant carries enough context to produce a `Fix:`-prefixed message.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CategoryFinding {
    /// Standard message-based finding.
    Message {
        /// Operation ID associated with the finding.
        op_id: String,
        /// Enforcement pass that emitted this finding.
        pass: &'static str,
        /// Human-readable failure text.
        message: String,
        /// Optional source location.
        location: Option<FindingLocation>,
        /// Severity level if explicitly classified.
        severity: Option<String>,
        /// Whether this finding was produced while falling back to text scan
        /// because the source file could not be parsed.
        parse_error: bool,
    },
    /// Category A AST violation with a structured kind and mandatory location.
    CategoryAViolation {
        /// Operation ID associated with the finding.
        op_id: String,
        /// Kind of violation (e.g. "Vec::new", "dyn Trait").
        kind: String,
        /// Source location of the violation.
        location: FindingLocation,
        /// Human-readable failure text.
        message: String,
    },
}

impl CategoryFinding {
    /// Emit an operation-scoped category finding.
    ///
    /// Use this constructor when the violation is tied to a specific op
    /// rather than a source file or a global pattern. The resulting finding
    /// has no location and no severity until one is attached with the
    /// `with_*` builder methods.
    #[inline]
    pub fn op(op_id: &str, pass: &'static str, message: impl Into<String>) -> Self {
        Self::Message {
            op_id: op_id.to_string(),
            pass,
            message: message.into(),
            location: None,
            severity: None,
            parse_error: false,
        }
    }

    /// Emit a source-scoped Category B finding.
    ///
    /// Category B findings are typically discovered by scanning source files
    /// for forbidden patterns (dynamic dispatch, runtime registries, etc.).
    /// The path and line anchor the finding so that IDE integrations can
    /// jump straight to the offending construct.
    #[inline]
    pub fn located(
        pass: &'static str,
        path: &Path,
        line: usize,
        message: impl Into<String>,
    ) -> Self {
        Self::Message {
            op_id: "CategoryB".to_string(),
            pass,
            message: message.into(),
            location: Some(FindingLocation::new(path, line)),
            severity: None,
            parse_error: false,
        }
    }

    /// Emit a Category A AST violation.
    ///
    /// Category A requires zero-overhead composition. If an op's source
    /// contains a construct that cannot be lowered to exactly the hand-written
    /// baseline (e.g. a heap allocation, a virtual call, or a hidden wrapper),
    /// this constructor produces a structured violation with a mandatory
    /// source location.
    #[inline]
    pub fn category_a_violation(
        op_id: &str,
        kind: impl Into<String>,
        location: FindingLocation,
    ) -> Self {
        let kind = kind.into();
        let message = format!(
            "contains forbidden {kind} at {}:{}. Fix: remove the construct to maintain Category A zero-overhead guarantee.",
            location.file, location.line
        );
        Self::CategoryAViolation {
            op_id: op_id.to_string(),
            kind,
            location,
            message,
        }
    }

    /// Operation ID associated with this finding.
    ///
    /// For `Message` variants this is the explicit `op_id`. For
    /// `CategoryAViolation` variants it is also the explicit `op_id`.
    /// Use this to filter findings by operation when rendering per-op
    /// reports.
    #[inline]
    pub fn op_id(&self) -> &str {
        match self {
            Self::Message { op_id, .. } => op_id,
            Self::CategoryAViolation { op_id, .. } => op_id,
        }
    }

    /// Enforcement pass that emitted this finding.
    ///
    /// The pass name is a stable snake_case identifier used by CI dashboards
    /// to route findings to the right owner (e.g. `category_a_zero_overhead`
    /// goes to the op author, `category_b_tripwire` goes to the architecture
    /// gate maintainer).
    #[inline]
    pub fn pass(&self) -> &'static str {
        match self {
            Self::Message { pass, .. } => pass,
            Self::CategoryAViolation { .. } => "category_a_zero_overhead",
        }
    }

    /// Human-readable failure text.
    ///
    /// This is the string that appears in CLI output and CI logs. It always
    /// includes a `Fix:` prefix when produced by the standard constructors.
    #[inline]
    pub fn message(&self) -> &str {
        match self {
            Self::Message { message, .. } => message,
            Self::CategoryAViolation { message, .. } => message,
        }
    }

    /// Source location if available.
    ///
    /// Returns `Some` for located findings and `CategoryAViolation`
    /// variants. Returns `None` for plain `Message` findings that were
    /// emitted without a file path.
    #[inline]
    pub fn location(&self) -> Option<&FindingLocation> {
        match self {
            Self::Message { location, .. } => location.as_ref(),
            Self::CategoryAViolation { location, .. } => Some(location),
        }
    }

    /// Severity level if explicitly classified.
    ///
    /// The category gate does not use severity levels — every finding is a
    /// hard failure. This field exists for compatibility with downstream
    /// report formats that may layer additional classification on top.
    #[inline]
    pub fn severity(&self) -> Option<&str> {
        match self {
            Self::Message { severity, .. } => severity.as_deref(),
            Self::CategoryAViolation { .. } => None,
        }
    }

    /// Whether this finding was produced while bypassing the AST visitor
    /// because of a parse error.
    ///
    /// Parse-error fallback is a safety net: if a source file cannot be
    /// parsed, the gate falls back to a text scan. A `parse_error` flag
    /// tells the consumer that the finding may be a false positive caused
    /// by macro-generated or syntactically unusual code.
    #[inline]
    pub fn parse_error(&self) -> bool {
        match self {
            Self::Message { parse_error, .. } => *parse_error,
            Self::CategoryAViolation { .. } => false,
        }
    }

    /// Set the severity level.
    ///
    /// This is a builder-style setter for report generators that need to
    /// annotate a finding after construction. It has no effect on
    /// `CategoryAViolation` variants because those are always hard failures.
    #[inline]
    pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
        match &mut self {
            Self::Message { severity: s, .. } => {
                *s = Some(severity.into());
            }
            Self::CategoryAViolation { .. } => {}
        }
        self
    }

    /// Mark whether this finding was produced while bypassing the AST visitor
    /// because of a parse error.
    ///
    /// Use this to flag findings that came from the fallback text scanner.
    /// It has no effect on `CategoryAViolation` variants.
    #[inline]
    pub fn with_parse_error(mut self, parse_error: bool) -> Self {
        match &mut self {
            Self::Message { parse_error: p, .. } => {
                *p = parse_error;
            }
            Self::CategoryAViolation { .. } => {}
        }
        self
    }
}

impl fmt::Display for CategoryFinding {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Message {
                op_id,
                location: Some(location),
                message,
                ..
            } => {
                write!(
                    f,
                    "{} declares Category B pattern at {}:{}: {}",
                    op_id, location.file, location.line, message
                )
            }
            Self::Message {
                op_id,
                location: None,
                message,
                ..
            } => {
                write!(f, "Op {} {}", op_id, message)
            }
            Self::CategoryAViolation {
                op_id,
                kind,
                location,
                message,
            } => {
                write!(
                    f,
                    "Op {} declares Category A violation ({}) at {}:{}: {}",
                    op_id, kind, location.file, location.line, message
                )
            }
        }
    }
}