Skip to main content

rhdl_ed25519_sim/
lib.rs

1//! Simulation report, waveform, Verilog-normalization, and checksum utilities.
2//!
3//! The `run_simulations` binary drives real top-level commands and message
4//! handshakes through the RHDL simulator and checks results against
5//! `rhdl_ed25519_model`. The `export_verilog` binary emits synthesizable RTL.
6//! This library keeps generated evidence deterministic and records the pinned
7//! Dalek/RHDL source commits beside every summary.
8
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16/// One named simulation check in a machine-readable report.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SimCase {
19    /// Stable case name.
20    pub name: String,
21    /// Usually `pass` or `fail`.
22    pub status: String,
23    /// Human-readable vector, cycle, counter, or failure detail.
24    pub detail: String,
25}
26
27/// Complete simulation summary and source pins.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SimReport {
30    /// Vendored Dalek commit used as semantic oracle.
31    pub dalek_commit: String,
32    /// Vendored RHDL commit used for simulation/lowering.
33    pub rhdl_commit: String,
34    /// Ordered simulation results.
35    pub cases: Vec<SimCase>,
36}
37
38/// Compact state transition recorded for VCD/SVG generation.
39#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
40pub struct TraceEvent {
41    /// Simulator callback index.
42    pub tick: u64,
43    /// Numeric state encoding at this tick.
44    pub state: u32,
45    /// Trace-specific auxiliary counter or value.
46    pub aux: u64,
47}
48
49/// Return the workspace-relative generated-report directory.
50pub fn generated_dir() -> PathBuf {
51    PathBuf::from("reports/generated")
52}
53
54/// Create and return [`generated_dir`].
55pub fn ensure_generated_dir() -> Result<PathBuf> {
56    let dir = generated_dir();
57    fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
58    Ok(dir)
59}
60
61/// Compute a lowercase SHA-256 checksum for one file.
62pub fn sha256_file(path: &Path) -> Result<String> {
63    let data = fs::read(path).with_context(|| format!("reading {}", path.display()))?;
64    let digest = Sha256::digest(&data);
65    Ok(hex::encode(digest))
66}
67
68/// Write deterministic SHA-256 rows for every regular file in `dir`.
69pub fn write_checksum_manifest(dir: &Path) -> Result<PathBuf> {
70    let mut rows = Vec::new();
71    let mut entries = fs::read_dir(dir)
72        .with_context(|| format!("reading {}", dir.display()))?
73        .collect::<Result<Vec<_>, _>>()?;
74    entries.sort_by_key(|entry| entry.path());
75    for entry in entries {
76        let path = entry.path();
77        if path.is_file() {
78            let name = path.file_name().unwrap().to_string_lossy().to_string();
79            if name == "checksums.sha256" {
80                continue;
81            }
82            rows.push(format!("{}  {}", sha256_file(&path)?, name));
83        }
84    }
85    let out = dir.join("checksums.sha256");
86    fs::write(&out, rows.join("\n") + "\n")
87        .with_context(|| format!("writing {}", out.display()))?;
88    Ok(out)
89}
90
91/// Rewrite over-wide exact-width Verilog binary literals as concatenations.
92///
93/// This preserves bit order while avoiding parser limits in downstream tools.
94pub fn split_wide_binary_literals(source: &str, max_digits: usize) -> String {
95    assert!(max_digits > 0);
96    let bytes = source.as_bytes();
97    let mut output = String::with_capacity(source.len());
98    let mut copied_until = 0;
99    let mut cursor = 0;
100
101    while cursor < bytes.len() {
102        if !bytes[cursor].is_ascii_digit()
103            || (cursor > 0
104                && (bytes[cursor - 1].is_ascii_alphanumeric() || bytes[cursor - 1] == b'_'))
105        {
106            cursor += 1;
107            continue;
108        }
109
110        let width_start = cursor;
111        while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
112            cursor += 1;
113        }
114        if cursor + 2 > bytes.len()
115            || bytes[cursor] != b'\''
116            || !matches!(bytes[cursor + 1], b'b' | b'B')
117        {
118            continue;
119        }
120
121        let digits_start = cursor + 2;
122        let mut digits_end = digits_start;
123        while digits_end < bytes.len()
124            && matches!(
125                bytes[digits_end],
126                b'0' | b'1' | b'x' | b'X' | b'z' | b'Z' | b'?' | b'_'
127            )
128        {
129            digits_end += 1;
130        }
131        let digits = &source[digits_start..digits_end];
132        let compact = digits
133            .bytes()
134            .filter(|byte| *byte != b'_')
135            .collect::<Vec<_>>();
136        let Ok(declared_width) = source[width_start..cursor].parse::<usize>() else {
137            cursor = digits_end;
138            continue;
139        };
140        if compact.len() <= max_digits || compact.len() != declared_width {
141            cursor = digits_end;
142            continue;
143        }
144
145        output.push_str(&source[copied_until..width_start]);
146        output.push('{');
147        for (index, chunk) in compact.chunks(max_digits).enumerate() {
148            if index > 0 {
149                output.push_str(",\n");
150            }
151            output.push_str(&chunk.len().to_string());
152            output.push_str("'b");
153            output.push_str(std::str::from_utf8(chunk).expect("binary literals are ASCII"));
154        }
155        output.push('}');
156        copied_until = digits_end;
157        cursor = digits_end;
158    }
159
160    output.push_str(&source[copied_until..]);
161    output
162}
163
164/// Write a compact digital-lane SVG used in generated simulation reports.
165pub fn write_waveform_svg(path: &Path, title: &str, lanes: &[(&str, &str)]) -> Result<()> {
166    let height = 72 + lanes.len() as i32 * 42;
167    let mut body = String::new();
168    body.push_str(&format!(
169        r##"<svg xmlns="http://www.w3.org/2000/svg" width="980" height="{height}" viewBox="0 0 980 {height}">
170<rect width="980" height="{height}" fill="#f9fafb"/>
171<text x="28" y="36" font-family="monospace" font-size="20" fill="#111827">{title}</text>
172<line x1="170" y1="54" x2="940" y2="54" stroke="#9ca3af" stroke-width="1"/>
173"##
174    ));
175    for (idx, (name, pattern)) in lanes.iter().enumerate() {
176        let y = 84 + idx as i32 * 42;
177        body.push_str(&format!(
178            r##"<text x="28" y="{y}" font-family="monospace" font-size="14" fill="#111827">{name}</text>"##
179        ));
180        let mut x = 176;
181        let mut high = false;
182        for ch in pattern.chars() {
183            let next_high = ch == '1' || ch == 'H';
184            let yy = if next_high { y - 18 } else { y - 4 };
185            if high != next_high {
186                let prev_yy = if high { y - 18 } else { y - 4 };
187                body.push_str(&format!(
188                    r##"<line x1="{x}" y1="{prev_yy}" x2="{x}" y2="{yy}" stroke="#2563eb" stroke-width="2"/>"##
189                ));
190            }
191            body.push_str(&format!(
192                r##"<line x1="{x}" y1="{yy}" x2="{}" y2="{yy}" stroke="#2563eb" stroke-width="2"/>"##,
193                x + 28
194            ));
195            high = next_high;
196            x += 28;
197        }
198        body.push_str(&format!(
199            r##"<line x1="170" y1="{}" x2="940" y2="{}" stroke="#e5e7eb" stroke-width="1"/>"##,
200            y + 8,
201            y + 8
202        ));
203    }
204    body.push_str("</svg>\n");
205    fs::write(path, body).with_context(|| format!("writing {}", path.display()))?;
206    Ok(())
207}
208
209/// Write a small standards-compliant VCD from sparse trace events.
210pub fn write_compact_vcd(
211    path: &Path,
212    scope: &str,
213    state_width: usize,
214    aux_name: &str,
215    aux_width: usize,
216    events: &[TraceEvent],
217    total_ticks: u64,
218) -> Result<()> {
219    let mut body = format!(
220        "$date\n  generated by rhdl_ed25519_sim\n$end\n\
221$timescale 1ns $end\n$scope module {scope} $end\n\
222$var wire {state_width} ! state $end\n\
223$var wire {aux_width} \" {aux_name} $end\n\
224$var wire 1 # done $end\n$upscope $end\n$enddefinitions $end\n"
225    );
226    body.push_str("#0\n0#\n");
227    for event in events {
228        body.push_str(&format!(
229            "#{}\nb{:0state_width$b} !\nb{:0aux_width$b} \"\n",
230            event.tick * 10,
231            event.state,
232            event.aux,
233        ));
234    }
235    body.push_str(&format!("#{}\n1#\n", total_ticks.saturating_mul(10)));
236    fs::write(path, body).with_context(|| format!("writing {}", path.display()))?;
237    Ok(())
238}
239
240/// Write an SVG timeline whose segments are labeled controller states.
241pub fn write_state_timeline_svg(
242    path: &Path,
243    title: &str,
244    events: &[TraceEvent],
245    total_ticks: u64,
246    state_names: &[&str],
247) -> Result<()> {
248    let colors = [
249        "#2563eb", "#059669", "#d97706", "#dc2626", "#7c3aed", "#0891b2", "#4d7c0f", "#be185d",
250    ];
251    let left = 42.0;
252    let right = 1058.0;
253    let plot_width = right - left;
254    let total = total_ticks.max(1) as f64;
255    let mut body = format!(
256        r##"<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="250" viewBox="0 0 1100 250">
257<rect width="1100" height="250" fill="#f8fafc"/>
258<text x="42" y="38" font-family="sans-serif" font-size="22" font-weight="700" fill="#111827">{title}</text>
259<text x="42" y="61" font-family="monospace" font-size="12" fill="#475569">{total_ticks} simulator callbacks</text>
260<rect x="42" y="86" width="1016" height="70" fill="#e2e8f0"/>
261"##
262    );
263    for (index, event) in events.iter().enumerate() {
264        let end = events
265            .get(index + 1)
266            .map(|next| next.tick)
267            .unwrap_or(total_ticks);
268        let x = left + event.tick as f64 / total * plot_width;
269        let segment_width = ((end.saturating_sub(event.tick)) as f64 / total * plot_width).max(1.0);
270        let color = colors[event.state as usize % colors.len()];
271        body.push_str(&format!(
272            r##"<rect x="{x:.2}" y="86" width="{segment_width:.2}" height="70" fill="{color}"/>"##
273        ));
274        if segment_width > 54.0 {
275            let label = state_names
276                .get(event.state as usize)
277                .copied()
278                .unwrap_or("unknown");
279            body.push_str(&format!(
280                r##"<text x="{:.2}" y="126" text-anchor="middle" font-family="sans-serif" font-size="11" fill="white">{label}</text>"##,
281                x + segment_width / 2.0
282            ));
283        }
284    }
285    for marker in 0..=4 {
286        let x = left + plot_width * marker as f64 / 4.0;
287        let tick = total_ticks * marker / 4;
288        body.push_str(&format!(
289            r##"<line x1="{x:.2}" y1="160" x2="{x:.2}" y2="169" stroke="#64748b"/><text x="{x:.2}" y="187" text-anchor="middle" font-family="monospace" font-size="11" fill="#475569">{tick}</text>"##
290        ));
291    }
292    body.push_str(&format!(
293        r##"<text x="42" y="225" font-family="sans-serif" font-size="12" fill="#334155">Trace events: {}. Auxiliary values are retained in the companion VCD.</text></svg>
294"##,
295        events.len()
296    ));
297    fs::write(path, body).with_context(|| format!("writing {}", path.display()))?;
298    Ok(())
299}
300
301#[cfg(test)]
302mod tests {
303    use super::split_wide_binary_literals;
304
305    #[test]
306    fn splits_only_exact_width_binary_literals() {
307        let source = "localparam x = 9'b101100111; localparam y = 4'b0011;";
308        let actual = split_wide_binary_literals(source, 4);
309        assert_eq!(
310            actual,
311            "localparam x = {4'b1011,\n4'b0011,\n1'b1}; localparam y = 4'b0011;"
312        );
313    }
314}