wavepeek 2.1.0

Command-line tool for RTL waveform inspection with deterministic machine-friendly output.
Documentation
use std::borrow::Cow;

use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::Serialize;

use crate::diagnostic::{Diagnostic, DiagnosticKind};
use crate::error::WavepeekError;
use crate::waveform::{STABLE_SCOPE_KIND_ALIASES, STABLE_SIGNAL_KIND_ALIASES};

#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ContractDiagnostic<'a> {
    Info {
        message: &'a str,
    },
    Warning {
        code: &'static str,
        message: &'a str,
    },
    Error {
        code: &'static str,
        message: &'a str,
    },
}

impl JsonSchema for ContractDiagnostic<'_> {
    fn schema_name() -> Cow<'static, str> {
        "diagnostic".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "object",
            "additionalProperties": true,
            "required": ["kind", "message"],
            "properties": {
                "kind": {"type": "string", "enum": ["info", "warning", "error"]},
                "code": {"type": "string", "pattern": "^WPK-[WE][0-9]{4}$"},
                "message": {"type": "string"},
            },
            "allOf": [
                {
                    "if": {"properties": {"kind": {"const": "warning"}}, "required": ["kind"]},
                    "then": {"required": ["code"], "properties": {"code": {"pattern": "^WPK-W[0-9]{4}$"}}},
                },
                {
                    "if": {"properties": {"kind": {"const": "error"}}, "required": ["kind"]},
                    "then": {"required": ["code"], "properties": {"code": {"pattern": "^WPK-E[0-9]{4}$"}}},
                },
                {
                    "if": {"properties": {"kind": {"const": "info"}}, "required": ["kind"]},
                    "then": {"not": {"required": ["code"]}},
                },
            ],
        })
    }
}

impl<'a> ContractDiagnostic<'a> {
    pub fn from_diagnostic(diagnostic: &'a Diagnostic) -> Result<Self, WavepeekError> {
        match diagnostic.kind() {
            DiagnosticKind::Info => Ok(Self::Info {
                message: diagnostic.message(),
            }),
            DiagnosticKind::Warning => Ok(Self::Warning {
                code: diagnostic.code().ok_or_else(|| {
                    WavepeekError::Internal(
                        "warning diagnostic is missing a stable code".to_string(),
                    )
                })?,
                message: diagnostic.message(),
            }),
            DiagnosticKind::Error => Ok(Self::Error {
                code: diagnostic.code().ok_or_else(|| {
                    WavepeekError::Internal("error diagnostic is missing a stable code".to_string())
                })?,
                message: diagnostic.message(),
            }),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub struct NormalizedTime<'a>(&'a str);

impl<'a> NormalizedTime<'a> {
    pub fn new(value: &'a str) -> Self {
        Self(value)
    }
}

impl JsonSchema for NormalizedTime<'_> {
    fn schema_name() -> Cow<'static, str> {
        "normalizedTime".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "string",
            "description": "Normalized timestamp rendered in the dump's time unit, for example 10ns."
        })
    }
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub struct CanonicalPath<'a>(&'a str);

impl<'a> CanonicalPath<'a> {
    pub fn new(value: &'a str) -> Self {
        Self(value)
    }
}

impl JsonSchema for CanonicalPath<'_> {
    fn schema_name() -> Cow<'static, str> {
        "canonicalPath".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "string",
            "description": "Canonical dot-separated hierarchy path emitted by wavepeek for a scope or signal."
        })
    }
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub struct SampledValue<'a>(&'a str);

impl<'a> SampledValue<'a> {
    pub fn new(value: &'a str) -> Self {
        Self(value)
    }
}

impl JsonSchema for SampledValue<'_> {
    fn schema_name() -> Cow<'static, str> {
        "sampledValue".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "string",
            "description": "Stable sampled signal value formatted as a Verilog-style literal string."
        })
    }
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub struct ScopeKind<'a>(&'a str);

impl JsonSchema for ScopeKind<'_> {
    fn schema_name() -> Cow<'static, str> {
        "scopeKind".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "string",
            "description": "Stable scope kind alias emitted by wavepeek for the selected scope.",
            "enum": STABLE_SCOPE_KIND_ALIASES,
        })
    }
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub struct SignalKind<'a>(&'a str);

impl JsonSchema for SignalKind<'_> {
    fn schema_name() -> Cow<'static, str> {
        "signalKind".into()
    }

    fn json_schema(_: &mut SchemaGenerator) -> Schema {
        json_schema!({
            "type": "string",
            "description": "Stable signal kind alias emitted by wavepeek for the selected signal.",
            "enum": STABLE_SIGNAL_KIND_ALIASES,
        })
    }
}

pub fn validate_scope_kind(kind: &str) -> Result<ScopeKind<'_>, WavepeekError> {
    if STABLE_SCOPE_KIND_ALIASES.contains(&kind) {
        Ok(ScopeKind(kind))
    } else {
        Err(WavepeekError::Internal(format!(
            "scope kind {kind:?} is not part of the stable machine-output contract"
        )))
    }
}

pub fn validate_signal_kind(kind: &str) -> Result<SignalKind<'_>, WavepeekError> {
    if STABLE_SIGNAL_KIND_ALIASES.contains(&kind) {
        Ok(SignalKind(kind))
    } else {
        Err(WavepeekError::Internal(format!(
            "signal kind {kind:?} is not part of the stable machine-output contract"
        )))
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::diagnostic::{Diagnostic, WarningDiagnosticCode};

    use super::{ContractDiagnostic, validate_scope_kind, validate_signal_kind};

    #[test]
    fn contract_diagnostic_preserves_wire_shape() {
        let diagnostic = Diagnostic::warning(WarningDiagnosticCode::OutputTruncated, "truncated");
        let contract = ContractDiagnostic::from_diagnostic(&diagnostic)
            .expect("warning diagnostic should convert");

        assert_eq!(
            serde_json::to_value(contract).expect("contract diagnostic should serialize"),
            json!({"kind": "warning", "code": "WPK-W0002", "message": "truncated"})
        );
    }

    #[test]
    fn kind_validation_rejects_backend_specific_aliases() {
        assert_eq!(
            serde_json::to_value(validate_scope_kind("module").unwrap()).unwrap(),
            json!("module")
        );
        assert_eq!(
            serde_json::to_value(validate_signal_kind("wire").unwrap()).unwrap(),
            json!("wire")
        );
        assert!(validate_scope_kind("vhdl_architecture").is_err());
        assert!(validate_signal_kind("std_logic").is_err());
    }
}