1use std::fs;
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SimCase {
19 pub name: String,
21 pub status: String,
23 pub detail: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SimReport {
30 pub dalek_commit: String,
32 pub rhdl_commit: String,
34 pub cases: Vec<SimCase>,
36}
37
38#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
40pub struct TraceEvent {
41 pub tick: u64,
43 pub state: u32,
45 pub aux: u64,
47}
48
49pub fn generated_dir() -> PathBuf {
51 PathBuf::from("reports/generated")
52}
53
54pub 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
61pub 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
68pub 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
91pub 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
164pub 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
209pub 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
240pub 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}