wavepeek 0.5.0

Command-line tool for RTL waveform inspection with deterministic machine-friendly output.
Documentation
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;

use crate::error::WavepeekError;
use crate::expr::{
    DiagnosticLayer, ExprDiagnostic, ExprType, ExpressionHost, SampledValue, SignalHandle, Span,
};
use crate::waveform::{ExprResolvedSignal, Waveform};

#[derive(Debug)]
pub(crate) struct WaveformExprHost {
    waveform: Rc<RefCell<Waveform>>,
    handles_by_name: RefCell<HashMap<String, SignalHandle>>,
    signals_by_handle: RefCell<HashMap<SignalHandle, Rc<ExprResolvedSignal>>>,
    next_handle: Cell<u32>,
}

impl WaveformExprHost {
    pub(crate) fn open(path: &Path) -> Result<Self, WavepeekError> {
        let waveform = Rc::new(RefCell::new(Waveform::open(path)?));
        Ok(Self::from_shared(waveform))
    }

    pub(crate) fn new(waveform: Waveform) -> Self {
        Self::from_shared(Rc::new(RefCell::new(waveform)))
    }

    pub(crate) fn from_shared(waveform: Rc<RefCell<Waveform>>) -> Self {
        Self {
            waveform,
            handles_by_name: RefCell::new(HashMap::new()),
            signals_by_handle: RefCell::new(HashMap::new()),
            next_handle: Cell::new(1),
        }
    }

    fn resolved_signal(
        &self,
        handle: SignalHandle,
    ) -> Result<Rc<ExprResolvedSignal>, ExprDiagnostic> {
        self.signals_by_handle
            .borrow()
            .get(&handle)
            .cloned()
            .ok_or_else(|| ExprDiagnostic {
                layer: DiagnosticLayer::Semantic,
                code: "HOST-UNKNOWN-SIGNAL",
                message: format!("unknown signal handle {}", handle.0),
                primary_span: Span::new(0, 0),
                notes: vec![],
            })
    }

    pub(crate) fn resolved_signal_for_handle(
        &self,
        handle: SignalHandle,
    ) -> Result<ExprResolvedSignal, WavepeekError> {
        Ok((*self
            .resolved_signal(handle)
            .map_err(|diagnostic| WavepeekError::Internal(diagnostic.message))?)
        .clone())
    }
}

impl ExpressionHost for WaveformExprHost {
    fn resolve_signal(&self, name: &str) -> Result<SignalHandle, ExprDiagnostic> {
        if let Some(handle) = self.handles_by_name.borrow().get(name).copied() {
            return Ok(handle);
        }

        let resolved = self
            .waveform
            .borrow()
            .resolve_expr_signal(name)
            .map_err(|error| ExprDiagnostic {
                layer: DiagnosticLayer::Semantic,
                code: "HOST-UNKNOWN-SIGNAL",
                message: error.to_string(),
                primary_span: Span::new(0, 0),
                notes: vec![],
            })?;

        let handle = SignalHandle(self.next_handle.get());
        self.next_handle.set(handle.0 + 1);
        self.handles_by_name
            .borrow_mut()
            .insert(name.to_string(), handle);
        self.signals_by_handle
            .borrow_mut()
            .insert(handle, Rc::new(resolved));
        Ok(handle)
    }

    fn signal_type(&self, handle: SignalHandle) -> Result<ExprType, ExprDiagnostic> {
        Ok(self.resolved_signal(handle)?.expr_type.clone())
    }

    fn sample_value(
        &self,
        handle: SignalHandle,
        timestamp: u64,
    ) -> Result<SampledValue, ExprDiagnostic> {
        let resolved = self.resolved_signal(handle)?;
        self.waveform
            .borrow_mut()
            .sample_expr_value(&resolved, timestamp)
            .map_err(|error| ExprDiagnostic {
                layer: DiagnosticLayer::Runtime,
                code: "HOST-SAMPLE-ERROR",
                message: error.to_string(),
                primary_span: Span::new(0, 0),
                notes: vec![],
            })
    }

    fn event_occurred(&self, handle: SignalHandle, timestamp: u64) -> Result<bool, ExprDiagnostic> {
        let resolved = self.resolved_signal(handle)?;
        self.waveform
            .borrow_mut()
            .expr_event_occurred(&resolved, timestamp)
            .map_err(|error| ExprDiagnostic {
                layer: DiagnosticLayer::Runtime,
                code: "HOST-EVENT-ERROR",
                message: error.to_string(),
                primary_span: Span::new(0, 0),
                notes: vec![],
            })
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::NamedTempFile;

    use super::WaveformExprHost;
    use crate::expr::{
        ExprValuePayload, bind_logical_expr_ast, eval_logical_expr_at, parse_logical_expr_ast,
    };

    const RICH_VCD: &str = concat!(
        "$date\n  today\n$end\n",
        "$version\n  wavepeek-expr-rich-types\n$end\n",
        "$timescale 1ns $end\n",
        "$scope module top $end\n",
        "$var wire 1 ! clk $end\n",
        "$var event 1 \" ev $end\n",
        "$scope module ev $end\n",
        "$var wire 1 & triggered $end\n",
        "$upscope $end\n",
        "$var real 1 # temp $end\n",
        "$var string 1 $ msg $end\n",
        "$var enum 2 % state $end\n",
        "$upscope $end\n",
        "$enddefinitions $end\n",
        "#0\n",
        "0!\n",
        "1&\n",
        "r0.5 #\n",
        "sidle $\n",
        "b00 %\n",
        "#10\n",
        "1!\n",
        "1\"\n",
        "0&\n",
        "r1.5 #\n",
        "sgo $\n",
        "b01 %\n",
        "#20\n",
        "0!\n",
        "1&\n",
        "r1.5 #\n",
        "shold $\n",
        "b10 %\n",
    );

    #[test]
    fn waveform_expr_host_distinguishes_triggered_signal_suffix_from_raw_event_call() {
        for filename in [
            "expr_triggered_collision.vcd",
            "expr_triggered_collision.fst",
        ] {
            let host =
                WaveformExprHost::open(&fixture_path(filename)).expect("fixture should open");
            let ast = parse_logical_expr_ast("!top.ev.triggered && top.ev.triggered()")
                .expect("expression should parse");
            let bound = bind_logical_expr_ast(&ast, &host).expect("expression should bind");
            let value =
                eval_logical_expr_at(&bound, &host, 10).expect("expression should evaluate");

            assert!(matches!(
                value.payload,
                ExprValuePayload::Integral { ref bits, .. } if bits == "1"
            ));
        }
    }

    #[test]
    fn waveform_expr_host_supports_recovered_bit_vector_cast_on_vcd_and_fst() {
        for filename in ["m2_core.vcd", "m2_core.fst"] {
            let host =
                WaveformExprHost::open(&fixture_path(filename)).expect("fixture should open");
            let ast =
                parse_logical_expr_ast("type(top.data)'(3)").expect("expression should parse");
            let bound = bind_logical_expr_ast(&ast, &host).expect("expression should bind");
            let value = eval_logical_expr_at(&bound, &host, 0).expect("expression should evaluate");
            assert!(matches!(
                value.payload,
                ExprValuePayload::Integral { ref bits, .. } if bits == "00000011"
            ));
        }
    }

    #[test]
    fn waveform_expr_host_reports_missing_enum_metadata_on_vcd() {
        let fixture = write_fixture(RICH_VCD, "rich-types.vcd");
        let host = WaveformExprHost::open(fixture.path()).expect("fixture should open");
        let ast = parse_logical_expr_ast("type(top.state)::BUSY").expect("expression should parse");
        let error = bind_logical_expr_ast(&ast, &host).expect_err("bind should fail");

        assert_eq!(error.code, "EXPR-SEMANTIC-METADATA");
    }

    fn fixture_path(filename: &str) -> std::path::PathBuf {
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests")
            .join("fixtures")
            .join("hand")
            .join(filename)
    }

    fn write_fixture(contents: &str, suffix: &str) -> NamedTempFile {
        let fixture = NamedTempFile::with_suffix(suffix).expect("temp fixture should create");
        fs::write(fixture.path(), contents).expect("fixture should write");
        fixture
    }
}