1use std::collections::HashMap;
11
12use color_eyre::Result;
13use polars::prelude::*;
14
15use crate::formats::model_files::MetaValue;
16use crate::formats::text_formats::{Detail, capped_list, count, note};
17use crate::notes::Note;
18
19pub(crate) const READER: crate::formats::readers::Reader = crate::formats::readers::Reader {
21 convert: Some(|input| {
22 crate::formats::text_formats::convert_with(input, VcdReader::new(), |reader, lf| {
23 Ok((lf, notes(reader), detail(reader)))
24 })
25 }),
26 scan: crate::formats::readers::read_into,
27 signatures: &[crate::formats::readers::Signature {
28 says: |head, _| looks_like(head),
29 kind: crate::formats::readers::Kind::Text,
30 trusted: crate::formats::readers::Trusted {
31 listing: false,
32 ..crate::formats::readers::EVERYWHERE
33 },
34 }],
35 ..crate::formats::readers::BASE
36};
37
38pub const MAX_TOKEN: usize = 1 << 20;
40pub const MAX_TEXT: usize = 4096;
42pub const MAX_DEPTH: usize = 256;
44pub const MAX_PATHS: usize = 64 << 20;
46const MAX_SECTION_TOKENS: usize = 16;
48pub const MAX_EXTEND: u32 = 4096;
50pub const BATCH_ROWS: usize = 65_536;
52pub const BATCH_TEXT: usize = 32 << 20;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Scale {
58 Nanos(i64),
60 Count { per_tick: i64, unit: &'static str },
62}
63
64impl Scale {
65 fn dtype(self) -> DataType {
66 match self {
67 Scale::Nanos(_) => DataType::Duration(TimeUnit::Nanoseconds),
68 Scale::Count { .. } => DataType::Int64,
69 }
70 }
71
72 fn per_tick(self) -> i64 {
73 match self {
74 Scale::Nanos(n) => n,
75 Scale::Count { per_tick, .. } => per_tick,
76 }
77 }
78}
79
80pub fn parse_timescale(text: &str) -> Option<Scale> {
82 let text: String = text.split_whitespace().collect();
83 let digits = text.find(|c: char| !c.is_ascii_digit())?;
84 let (number, unit) = text.split_at(digits);
85 let number: i64 = number.parse().ok()?;
86 if !matches!(number, 1 | 10 | 100) {
87 return None;
88 }
89 Some(match unit {
90 "s" => Scale::Nanos(number * 1_000_000_000),
91 "ms" => Scale::Nanos(number * 1_000_000),
92 "us" => Scale::Nanos(number * 1_000),
93 "ns" => Scale::Nanos(number),
94 "ps" => Scale::Count {
95 per_tick: number,
96 unit: "ps",
97 },
98 "fs" => Scale::Count {
99 per_tick: number,
100 unit: "fs",
101 },
102 _ => return None,
103 })
104}
105
106pub fn looks_like(head: &[u8]) -> bool {
109 let text = head.strip_prefix(b"\xef\xbb\xbf").unwrap_or(head);
110 let start = text
111 .iter()
112 .position(|b| !b.is_ascii_whitespace())
113 .unwrap_or(text.len());
114 let text = &text[start..];
115 let first = text
116 .split(|b| b.is_ascii_whitespace())
117 .next()
118 .unwrap_or_default();
119 matches!(
120 first,
121 b"$date" | b"$version" | b"$timescale" | b"$comment" | b"$scope" | b"$var"
122 ) && text.windows(4).any(|w| w == b"$end")
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct Var {
128 pub path: String,
130 pub kind: String,
132 pub width: u32,
133 pub id: String,
135}
136
137#[derive(Debug, Clone, Default, PartialEq)]
139pub struct Header {
140 pub date: Option<String>,
141 pub version: Option<String>,
142 pub timescale: Option<String>,
143 pub comments: Vec<String>,
144 pub scopes: u64,
145 pub vars: Vec<Var>,
146}
147
148#[derive(Debug, Clone, Default, PartialEq)]
150pub struct Stats {
151 pub rows: u64,
153 pub times: u64,
155 pub first_time: Option<i64>,
156 pub last_time: Option<i64>,
157 pub unreadable: u64,
159 pub undeclared: u64,
161 pub signals_dropped: u64,
163 pub vars_dropped: u64,
166 pub overflowed: u64,
168 pub backwards: u64,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173enum Section {
174 Date,
175 Version,
176 Timescale,
177 Comment,
178 Scope,
179 Upscope,
180 Var,
181 EndDefinitions,
182 Other,
183}
184
185#[derive(Debug, Clone, PartialEq)]
187enum Value {
188 Vector(String),
190 Real(String),
192 Text(String),
194 Scalar(char),
196}
197
198#[derive(Debug)]
199enum Pending {
200 Nothing,
201 Section {
202 what: Section,
203 tokens: Vec<String>,
204 text: usize,
205 },
206 Id(Value),
207}
208
209#[derive(Debug, Default)]
211struct Rows {
212 time: Vec<Option<i64>>,
213 var: Vec<u32>,
214 value: Vec<String>,
215 int: Vec<Option<u64>>,
216}
217
218#[derive(Debug)]
220pub struct VcdReader {
221 buf: Vec<u8>,
222 skipping: bool,
224 in_body: bool,
225 pending: Pending,
226 scope: Vec<String>,
227 too_deep: usize,
229 path_bytes: usize,
231 header: Header,
232 ids: HashMap<String, Vec<u32>>,
233 scale: Option<Scale>,
234 time: i64,
235 rows: Rows,
236 held: usize,
237 stats: Stats,
238 seen: bool,
240 max_signals: usize,
242}
243
244impl Default for VcdReader {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl VcdReader {
251 pub fn new() -> Self {
252 Self {
253 buf: Vec::new(),
254 skipping: false,
255 in_body: false,
256 pending: Pending::Nothing,
257 scope: Vec::new(),
258 too_deep: 0,
259 path_bytes: 0,
260 header: Header::default(),
261 ids: HashMap::new(),
262 scale: None,
263 time: 0,
264 rows: Rows::default(),
265 held: 0,
266 stats: Stats::default(),
267 seen: false,
268 max_signals: crate::limits::get().vcd_signals,
269 }
270 }
271
272 pub fn header(&self) -> &Header {
273 &self.header
274 }
275
276 pub fn stats(&self) -> &Stats {
277 &self.stats
278 }
279
280 pub fn scale(&self) -> Option<Scale> {
282 self.scale
283 }
284
285 pub fn schema(&self) -> Option<Schema> {
287 let scale = self.scale?;
288 Some(Schema::from_iter([
289 Field::new("time".into(), scale.dtype()),
290 Field::new("signal".into(), DataType::String),
291 Field::new("value".into(), DataType::String),
292 Field::new("int".into(), DataType::UInt64),
293 Field::new("width".into(), DataType::UInt32),
294 ]))
295 }
296
297 pub fn push(&mut self, bytes: &[u8]) {
299 let mut start = 0;
300 for (i, &b) in bytes.iter().enumerate() {
301 if !b.is_ascii_whitespace() {
302 continue;
303 }
304 if self.skipping {
305 self.skipping = false;
306 self.buf.clear();
307 } else if self.buf.len() + (i - start) <= MAX_TOKEN {
308 self.buf.extend_from_slice(&bytes[start..i]);
309 if !self.buf.is_empty() {
310 let token = std::mem::take(&mut self.buf);
311 self.token(&String::from_utf8_lossy(&token));
312 }
313 } else {
314 self.buf.clear();
315 self.stats.unreadable += 1;
316 }
317 start = i + 1;
318 }
319 let rest = &bytes[start..];
320 if self.skipping {
321 return;
322 }
323 if self.buf.len() + rest.len() > MAX_TOKEN {
324 self.buf.clear();
325 self.skipping = true;
326 self.stats.unreadable += 1;
327 } else {
328 self.buf.extend_from_slice(rest);
329 }
330 }
331
332 pub fn take_batch(&mut self) -> PolarsResult<Option<DataFrame>> {
334 if self.rows.var.len() < BATCH_ROWS && self.held < BATCH_TEXT {
335 return Ok(None);
336 }
337 self.batch().map(Some)
338 }
339
340 pub fn finish(&mut self) -> Result<DataFrame, String> {
342 if !self.skipping && !self.buf.is_empty() {
343 let token = std::mem::take(&mut self.buf);
344 self.token(&String::from_utf8_lossy(&token));
345 }
346 if !self.seen {
347 return Err("Not a VCD file: it has no $var, $timescale or #time.".into());
348 }
349 self.start_body();
350 self.batch().map_err(|e| e.to_string())
351 }
352
353 fn batch(&mut self) -> PolarsResult<DataFrame> {
354 self.held = 0;
355 let scale = self.scale.unwrap_or(Scale::Count {
356 per_tick: 1,
357 unit: "ticks",
358 });
359 let rows = std::mem::take(&mut self.rows);
360 let height = rows.var.len();
361 let time = Int64Chunked::from_iter_options("time".into(), rows.time.into_iter());
362 let time = match scale {
363 Scale::Nanos(_) => time.into_duration(TimeUnit::Nanoseconds).into_column(),
364 Scale::Count { .. } => time.into_column(),
365 };
366 let vars = &self.header.vars;
367 let signal = StringChunked::from_iter_values(
368 "signal".into(),
369 rows.var.iter().map(|&i| vars[i as usize].path.as_str()),
370 );
371 let width = UInt32Chunked::from_iter_values(
372 "width".into(),
373 rows.var.iter().map(|&i| vars[i as usize].width),
374 );
375 let value = StringChunked::from_iter_values("value".into(), rows.value.iter());
376 let int = UInt64Chunked::from_iter_options("int".into(), rows.int.into_iter());
377 DataFrame::new(
378 height,
379 vec![
380 time,
381 signal.into_column(),
382 value.into_column(),
383 int.into_column(),
384 width.into_column(),
385 ],
386 )
387 }
388
389 fn start_body(&mut self) {
390 if self.in_body {
391 return;
392 }
393 self.in_body = true;
394 self.scale = Some(
395 self.header
396 .timescale
397 .as_deref()
398 .and_then(parse_timescale)
399 .unwrap_or(Scale::Count {
400 per_tick: 1,
401 unit: "ticks",
402 }),
403 );
404 }
405
406 fn token(&mut self, token: &str) {
407 match std::mem::replace(&mut self.pending, Pending::Nothing) {
408 Pending::Section {
409 what,
410 mut tokens,
411 text,
412 } => {
413 if token == "$end" {
414 self.section(what, tokens);
415 } else {
416 let keep = match what {
417 Section::Date | Section::Version | Section::Comment => {
418 text + token.len() <= MAX_TEXT
419 }
420 Section::Timescale | Section::Scope | Section::Var => {
421 tokens.len() < MAX_SECTION_TOKENS && text + token.len() <= MAX_TEXT
422 }
423 _ => false,
424 };
425 let text = if keep {
426 tokens.push(token.to_string());
427 text + token.len()
428 } else {
429 text
430 };
431 self.pending = Pending::Section { what, tokens, text };
432 }
433 return;
434 }
435 Pending::Id(value) => {
436 self.change(value, token);
437 return;
438 }
439 Pending::Nothing => {}
440 }
441 if let Some(keyword) = token.strip_prefix('$') {
442 let what = match keyword {
443 "date" => Section::Date,
444 "version" => Section::Version,
445 "timescale" => Section::Timescale,
446 "comment" => Section::Comment,
447 "scope" => Section::Scope,
448 "upscope" => Section::Upscope,
449 "var" => Section::Var,
450 "enddefinitions" => Section::EndDefinitions,
451 "end" => return,
453 "dumpvars" | "dumpall" | "dumpon" | "dumpoff" if self.in_body => return,
455 _ if self.in_body => {
456 self.stats.unreadable += 1;
457 return;
458 }
459 _ => Section::Other,
460 };
461 if self.in_body && what != Section::Comment {
462 self.stats.unreadable += 1;
464 return;
465 }
466 self.seen = true;
467 self.pending = Pending::Section {
468 what,
469 tokens: Vec::new(),
470 text: 0,
471 };
472 return;
473 }
474 if let Some(digits) = token.strip_prefix('#') {
475 if let Ok(time) = digits.parse::<u64>()
476 && let Ok(time) = i64::try_from(time)
477 {
478 self.seen = true;
479 self.start_body();
480 if time < self.time && self.stats.times > 0 {
481 self.stats.backwards += 1;
482 }
483 self.time = time;
484 self.stats.times += 1;
485 self.stats.first_time.get_or_insert(time);
486 self.stats.last_time = Some(time);
487 } else {
488 self.stats.unreadable += 1;
489 }
490 return;
491 }
492 if !self.in_body {
493 self.stats.unreadable += 1;
494 return;
495 }
496 let mut chars = token.chars();
497 let Some(first) = chars.next() else {
498 return;
499 };
500 let rest = chars.as_str();
501 match first {
502 'b' | 'B' if !rest.is_empty() => self.pending = Pending::Id(Value::Vector(rest.into())),
503 'r' | 'R' if !rest.is_empty() => self.pending = Pending::Id(Value::Real(rest.into())),
504 's' | 'S' => self.pending = Pending::Id(Value::Text(rest.into())),
505 '0' | '1' | 'x' | 'X' | 'z' | 'Z' | 'u' | 'U' | 'w' | 'W' | 'l' | 'L' | 'h' | 'H'
506 | '-' => {
507 if rest.is_empty() {
508 self.pending = Pending::Id(Value::Scalar(first));
509 } else {
510 self.change(Value::Scalar(first), rest);
511 }
512 }
513 _ => self.stats.unreadable += 1,
514 }
515 }
516
517 fn section(&mut self, what: Section, tokens: Vec<String>) {
518 match what {
519 Section::Date => self.header.date = Some(tokens.join(" ")),
520 Section::Version => self.header.version = Some(tokens.join(" ")),
521 Section::Timescale => self.header.timescale = Some(tokens.join(" ")),
522 Section::Comment => {
523 if !self.in_body && self.header.comments.len() < 16 {
524 self.header.comments.push(tokens.join(" "));
525 }
526 }
527 Section::Scope => {
528 let name = tokens.last().cloned().unwrap_or_default();
530 if self.scope.len() < MAX_DEPTH && self.too_deep == 0 {
531 self.header.scopes += 1;
532 self.scope.push(name);
533 } else {
534 self.stats.vars_dropped += 1;
535 self.too_deep += 1;
536 }
537 }
538 Section::Upscope => {
539 if self.too_deep > 0 {
540 self.too_deep -= 1;
541 } else {
542 self.scope.pop();
543 }
544 }
545 Section::Var => self.var(tokens),
546 Section::EndDefinitions => self.start_body(),
547 Section::Other => {}
548 }
549 }
550
551 fn var(&mut self, tokens: Vec<String>) {
553 let [kind, width, id, name, range @ ..] = tokens.as_slice() else {
554 self.stats.unreadable += 1;
555 return;
556 };
557 let len = self.scope.iter().map(|s| s.len() + 1).sum::<usize>()
558 + name.len()
559 + range.iter().map(String::len).sum::<usize>();
560 if self.header.vars.len() >= self.max_signals {
561 self.stats.signals_dropped += 1;
562 return;
563 }
564 if self.too_deep > 0 || len > MAX_TEXT || self.path_bytes + len > MAX_PATHS {
565 self.stats.vars_dropped += 1;
566 return;
567 }
568 let Ok(width) = width.parse::<u32>() else {
569 self.stats.unreadable += 1;
570 return;
571 };
572 let mut path = String::new();
573 for scope in &self.scope {
574 path.push_str(scope);
575 path.push('.');
576 }
577 path.push_str(name);
578 for part in range {
579 path.push_str(part);
580 }
581 self.path_bytes += path.len();
582 let index = self.header.vars.len() as u32;
583 self.header.vars.push(Var {
584 path,
585 kind: kind.clone(),
586 width,
587 id: id.clone(),
588 });
589 self.ids.entry(id.clone()).or_default().push(index);
590 }
591
592 fn change(&mut self, value: Value, id: &str) {
593 let Some(vars) = self.ids.get(id) else {
594 self.stats.undeclared += 1;
595 return;
596 };
597 let time = match self.scale.unwrap_or(Scale::Nanos(1)).per_tick() {
598 1 => Some(self.time),
599 per => self.time.checked_mul(per),
600 };
601 if time.is_none() {
602 self.stats.overflowed += vars.len() as u64;
603 }
604 for &var in vars {
605 let width = self.header.vars[var as usize].width;
606 let (text, int) = match &value {
607 Value::Scalar(c) => (
608 c.to_string(),
609 match c {
610 '0' => Some(0),
611 '1' => Some(1),
612 _ => None,
613 },
614 ),
615 Value::Vector(bits) => {
616 let text = extend(bits, width);
617 let int = to_int(&text);
618 (text, int)
619 }
620 Value::Real(text) | Value::Text(text) => (text.clone(), None),
621 };
622 self.held += text.len();
623 self.rows.time.push(time);
624 self.rows.var.push(var);
625 self.rows.value.push(text);
626 self.rows.int.push(int);
627 self.stats.rows += 1;
628 }
629 }
630}
631
632fn extend(bits: &str, width: u32) -> String {
635 let bits = bits.to_ascii_lowercase();
636 let len = bits.chars().count();
637 let width = width.min(MAX_EXTEND) as usize;
638 if len >= width {
639 return bits;
640 }
641 let fill = bits
642 .chars()
643 .next()
644 .filter(|c| matches!(c, 'x' | 'z'))
645 .unwrap_or('0');
646 let mut out = String::with_capacity(width);
647 out.extend(std::iter::repeat_n(fill, width - len));
648 out.push_str(&bits);
649 out
650}
651
652fn to_int(bits: &str) -> Option<u64> {
654 if bits.is_empty() || !bits.bytes().all(|b| b == b'0' || b == b'1') {
655 return None;
656 }
657 let significant = bits.trim_start_matches('0');
658 if significant.len() > 64 {
659 return None;
660 }
661 if significant.is_empty() {
662 return Some(0);
663 }
664 u64::from_str_radix(significant, 2).ok()
665}
666
667fn notes(reader: &VcdReader) -> Vec<Note> {
669 let stats = reader.stats();
670 let mut notes = Vec::new();
671 let of_rows = format!("of {}", count(stats.rows, "value change", "value changes"));
672 if stats.unreadable > 0 {
673 notes.push(note(
674 format!(
675 "{} skipped: not VCD",
676 count(stats.unreadable, "token", "tokens")
677 ),
678 "in the whole file".to_string(),
679 ));
680 }
681 if stats.undeclared > 0 {
682 notes.push(note(
683 format!(
684 "{} left out: identifier not declared by a $var",
685 count(stats.undeclared, "value change", "value changes")
686 ),
687 of_rows.clone(),
688 ));
689 }
690 if stats.signals_dropped > 0 {
691 notes.push(note(
692 crate::limits::left_out(
693 &count(stats.signals_dropped, "signal", "signals"),
694 crate::limits::get().vcd_signals,
695 "vcd_signals",
696 ),
697 "in the header".to_string(),
698 ));
699 }
700 if stats.vars_dropped > 0 {
702 notes.push(note(
703 format!(
704 "{} left out: nested past {} scopes, or a name past {} bytes",
705 count(stats.vars_dropped, "declaration", "declarations"),
706 MAX_DEPTH,
707 group_u64(MAX_TEXT as u64)
708 ),
709 "in the header".to_string(),
710 ));
711 }
712 if stats.overflowed > 0 {
713 notes.push(note(
714 format!(
715 "{} past the Duration range {} time null",
716 count(stats.overflowed, "value change", "value changes"),
717 crate::glyphs::get().middot
718 ),
719 of_rows.clone(),
720 ));
721 }
722 if stats.backwards > 0 {
723 notes.push(note(
724 format!(
725 "{} earlier than the one before",
726 count(stats.backwards, "#time", "#times")
727 ),
728 format!("of {}", count(stats.times, "#time", "#times")),
729 ));
730 }
731 match reader.scale() {
732 Some(Scale::Count { unit: "ticks", .. }) => notes.push(note(
733 format!(
734 "no $timescale {} time in ticks",
735 crate::glyphs::get().middot
736 ),
737 "in the header".to_string(),
738 )),
739 Some(Scale::Count { unit, .. }) => notes.push(note(
740 format!(
741 "timescale finer than a Duration {} time in {unit}",
742 crate::glyphs::get().middot
743 ),
744 format!(
745 "from $timescale {}",
746 reader.header().timescale.as_deref().unwrap_or_default()
747 ),
748 )),
749 _ => {}
750 }
751 notes
752}
753
754fn group_u64(n: u64) -> String {
755 crate::numfmt::group_chrome(usize::try_from(n).unwrap_or(usize::MAX))
756}
757
758fn span_text(first: i64, last: i64, scale: Scale) -> String {
761 let (per, units): (i64, &[(&str, i64)]) = match scale {
762 Scale::Nanos(ns) => (
763 ns,
764 &[
765 ("s", 1_000_000_000),
766 ("ms", 1_000_000),
767 ("us", 1_000),
768 ("ns", 1),
769 ],
770 ),
771 Scale::Count { per_tick, unit } => (
772 per_tick,
773 if unit == "ps" {
774 &[("ps", 1)]
775 } else if unit == "fs" {
776 &[("fs", 1)]
777 } else {
778 &[("ticks", 1)]
779 },
780 ),
781 };
782 let (Some(a), Some(b)) = (first.checked_mul(per), last.checked_mul(per)) else {
783 return format!("#{first} to #{last}");
784 };
785 let (unit, div) = units
786 .iter()
787 .find(|(_, d)| a % d == 0 && b % d == 0)
788 .copied()
789 .unwrap_or(("ns", 1));
790 let show = |n: i64| {
791 let text = group_u64((n / div).unsigned_abs());
792 if n < 0 { format!("-{text}") } else { text }
793 };
794 format!("{} to {} {unit}", show(a), show(b))
795}
796
797pub fn detail(reader: &VcdReader) -> Detail {
799 let header = reader.header();
800 let stats = reader.stats();
801 let sep = format!(" {} ", crate::glyphs::get().middot);
802 let mut head = String::from("VCD");
803 if let Some(timescale) = &header.timescale {
804 head.push_str(&sep);
805 head.push_str(&format!("timescale {timescale}"));
806 }
807 head.push_str(&sep);
808 head.push_str(&count(header.vars.len() as u64, "signal", "signals"));
809 head.push_str(&format!(" in {}", count(header.scopes, "scope", "scopes")));
810 let mut lines = vec![head];
811 let mut body = count(stats.rows, "value change", "value changes");
812 if let (Some(first), Some(last), Some(scale)) =
813 (stats.first_time, stats.last_time, reader.scale())
814 {
815 body.push_str(&sep);
816 body.push_str(&span_text(first, last, scale));
817 }
818 lines.push(body);
819 if let Some(date) = header.date.as_deref().filter(|d| !d.is_empty()) {
820 lines.push(format!("Date: {date}"));
821 }
822 if let Some(version) = header.version.as_deref().filter(|v| !v.is_empty()) {
823 lines.push(format!("Version: {version}"));
824 }
825 for comment in header.comments.iter().filter(|c| !c.is_empty()) {
826 lines.push(format!("Comment: {comment}"));
827 }
828 let list = capped_list(
829 header.vars.iter().map(|v| {
830 (
831 v.path.clone(),
832 MetaValue::Text(format!(
833 "{}{sep}{}{sep}id {}",
834 v.kind,
835 count(u64::from(v.width), "bit", "bits"),
836 v.id
837 )),
838 )
839 }),
840 header.vars.len(),
841 );
842 Detail {
843 tab: crate::formats::text_formats::tab(crate::FileFormat::Vcd),
844 lines,
845 list_title: "Signals",
846 list,
847 first: true,
848 ..Default::default()
849 }
850}
851
852impl crate::formats::text_formats::BatchReader for VcdReader {
853 fn push(&mut self, piece: &[u8]) -> Result<()> {
854 self.push(piece);
855 Ok(())
856 }
857
858 fn take_batch(&mut self) -> PolarsResult<Option<DataFrame>> {
859 self.take_batch()
860 }
861
862 fn finish(&mut self) -> Result<DataFrame> {
863 self.finish().map_err(|e| color_eyre::eyre::eyre!(e))
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870
871 #[test]
873 fn errors_name_the_file() {
874 crate::formats::readers::bad_input::each_names_its_file(
875 crate::FileFormat::Vcd,
876 &[("words.vcd", b"hello there\n", "Not a VCD file")],
877 );
878 }
879
880 const SAMPLE: &str = "$date Mon Oct 2 2026 $end
881$version Icarus Verilog $end
882$timescale 1ns $end
883$scope module top $end
884$var wire 1 ! clk $end
885$scope module cpu $end
886$var wire 8 \" data [7:0] $end
887$var real 64 # temp $end
888$upscope $end
889$upscope $end
890$enddefinitions $end
891#0
892$dumpvars
8930!
894bx \"
895r20.5 #
896$end
897#5
8981!
899b101 \"
900#10
9010!
902b11111111 \"
903";
904
905 fn read(text: &[u8], piece: usize) -> (DataFrame, VcdReader) {
906 let mut reader = VcdReader::new();
907 let mut frames = Vec::new();
908 for chunk in text.chunks(piece) {
909 reader.push(chunk);
910 if let Some(df) = reader.take_batch().unwrap() {
911 frames.push(df);
912 }
913 }
914 frames.push(reader.finish().unwrap());
915 let mut df = frames.remove(0);
916 for f in frames {
917 df.vstack_mut(&f).unwrap();
918 }
919 (df, reader)
920 }
921
922 fn strings(df: &DataFrame, name: &str) -> Vec<String> {
923 df.column(name)
924 .unwrap()
925 .str()
926 .unwrap()
927 .iter()
928 .map(|s| s.unwrap_or_default().to_string())
929 .collect()
930 }
931
932 #[test]
933 fn a_dump_reads_as_its_value_changes() {
934 for piece in [1, 3, 7, 4096] {
935 let (df, reader) = read(SAMPLE.as_bytes(), piece);
936 assert_eq!(df.height(), 7, "piece {piece}");
937 assert_eq!(
938 strings(&df, "signal"),
939 [
940 "top.clk",
941 "top.cpu.data[7:0]",
942 "top.cpu.temp",
943 "top.clk",
944 "top.cpu.data[7:0]",
945 "top.clk",
946 "top.cpu.data[7:0]"
947 ]
948 );
949 assert_eq!(
950 strings(&df, "value"),
951 ["0", "xxxxxxxx", "20.5", "1", "00000101", "0", "11111111"]
952 );
953 let int: Vec<Option<u64>> = df.column("int").unwrap().u64().unwrap().iter().collect();
954 assert_eq!(
955 int,
956 [Some(0), None, None, Some(1), Some(5), Some(0), Some(255)]
957 );
958 let time = df.column("time").unwrap();
959 assert_eq!(time.dtype(), &DataType::Duration(TimeUnit::Nanoseconds));
960 let ns: Vec<Option<i64>> = time.duration().unwrap().physical().iter().collect();
961 assert_eq!(ns[3], Some(5));
962 assert_eq!(ns[6], Some(10));
963 let width: Vec<Option<u32>> =
964 df.column("width").unwrap().u32().unwrap().iter().collect();
965 assert_eq!(width[1], Some(8));
966 assert_eq!(reader.header().scopes, 2);
967 assert_eq!(reader.header().date.as_deref(), Some("Mon Oct 2 2026"));
968 assert_eq!(reader.stats().unreadable, 0);
969 assert_eq!(df.schema().as_ref(), &reader.schema().unwrap());
970 }
971 }
972
973 #[test]
974 fn timescales() {
975 assert_eq!(parse_timescale("1ns"), Some(Scale::Nanos(1)));
976 assert_eq!(parse_timescale("10 us"), Some(Scale::Nanos(10_000)));
977 assert_eq!(parse_timescale("100 ms"), Some(Scale::Nanos(100_000_000)));
978 assert_eq!(
979 parse_timescale("1ps"),
980 Some(Scale::Count {
981 per_tick: 1,
982 unit: "ps"
983 })
984 );
985 assert_eq!(parse_timescale("3ns"), None);
986 assert_eq!(parse_timescale("1 parsec"), None);
987 }
988
989 #[test]
990 fn a_picosecond_timescale_counts_picoseconds() {
991 let text = "$timescale 10ps $end $var wire 1 ! a $end $enddefinitions $end #3 1!";
992 let (df, reader) = read(text.as_bytes(), 5);
993 let time = df.column("time").unwrap();
994 assert_eq!(time.dtype(), &DataType::Int64);
995 assert_eq!(time.i64().unwrap().get(0), Some(30));
996 assert!(
997 notes(&reader)
998 .iter()
999 .any(|n| n.summary.contains("time in ps"))
1000 );
1001 }
1002
1003 #[test]
1004 fn aliases_undeclared_ids_and_garbage() {
1005 let text = "$timescale 1 us $end
1006$scope module a $end $var wire 1 ! x $end $upscope $end
1007$scope module b $end $var wire 1 ! y $end $upscope $end
1008$enddefinitions $end
1009#1 1! 0? %%% #2 z!";
1010 let (df, reader) = read(text.as_bytes(), 2);
1011 assert_eq!(strings(&df, "signal"), ["a.x", "b.y", "a.x", "b.y"]);
1012 assert_eq!(strings(&df, "value"), ["1", "1", "z", "z"]);
1013 assert_eq!(reader.stats().undeclared, 1);
1014 assert_eq!(reader.stats().unreadable, 1);
1015 let ns: Vec<Option<i64>> = df
1016 .column("time")
1017 .unwrap()
1018 .duration()
1019 .unwrap()
1020 .physical()
1021 .iter()
1022 .collect();
1023 assert_eq!(ns, [Some(1000), Some(1000), Some(2000), Some(2000)]);
1024 }
1025
1026 #[test]
1027 fn a_long_token_is_passed_over_and_a_wide_value_kept_whole() {
1028 let mut text = b"$var wire 100 ! w $end $enddefinitions $end #0 b".to_vec();
1029 text.extend(std::iter::repeat_n(b'1', 100));
1030 text.extend_from_slice(b" ! #1 b");
1031 text.extend(std::iter::repeat_n(b'0', MAX_TOKEN + 10));
1032 text.extend_from_slice(b" ! #2 b1 !");
1033 let (df, reader) = read(&text, 65536);
1034 assert_eq!(df.height(), 2);
1035 assert_eq!(strings(&df, "value")[0].len(), 100);
1036 assert_eq!(df.column("int").unwrap().u64().unwrap().get(0), None);
1037 assert_eq!(df.column("int").unwrap().u64().unwrap().get(1), Some(1));
1038 assert!(reader.stats().unreadable >= 1);
1040 }
1041
1042 #[test]
1043 fn not_vcd_is_an_error() {
1044 let mut reader = VcdReader::new();
1045 reader.push(b"hello world");
1046 assert!(reader.finish().is_err());
1047 assert!(!looks_like(b"$foo $end"));
1048 assert!(looks_like(b"\n$date\n today\n$end\n"));
1049 }
1050
1051 #[test]
1052 fn the_detail_names_the_header_and_signals() {
1053 let (_, reader) = read(SAMPLE.as_bytes(), 4096);
1054 let detail = detail(&reader);
1055 assert_eq!(detail.tab, "VCD");
1056 assert!(
1057 detail.lines[0].contains("timescale 1ns"),
1058 "{:?}",
1059 detail.lines
1060 );
1061 assert!(detail.lines[0].contains("3 signals in 2 scopes"));
1062 assert!(detail.lines[1].contains("0 to 10 ns"), "{:?}", detail.lines);
1063 assert_eq!(span_text(0, 800, Scale::Nanos(1_000_000_000)), "0 to 800 s");
1064 assert_eq!(span_text(5, 2_500, Scale::Nanos(1)), "5 to 2,500 ns");
1065 assert_eq!(
1066 span_text(
1067 4,
1068 8,
1069 Scale::Count {
1070 per_tick: 10,
1071 unit: "ps"
1072 }
1073 ),
1074 "40 to 80 ps"
1075 );
1076 assert_eq!(detail.list.len(), 3);
1077 assert_eq!(detail.list[1].0, "top.cpu.data[7:0]");
1078 }
1079}