1use std::collections::HashMap;
11use std::path::Path;
12
13use color_eyre::Result;
14use polars::prelude::*;
15
16use crate::OpenOptions;
17use crate::model_files::MetaValue;
18use crate::notes::Note;
19use crate::segments::{Converted, Segments};
20use crate::text_formats::{Detail, Pieces, capped_list, count, note};
21use crate::unfinished::Writer;
22
23pub(crate) const READER: crate::readers::Reader = crate::readers::Reader {
25 convert: Some(|input| {
26 crate::text_formats::read_one(input, |pieces| {
27 convert(input.display, input.options, input.writer, pieces)
28 })
29 }),
30 scan: crate::readers::read_into,
31 signatures: &[crate::readers::Signature {
32 says: |head, _| looks_like(head),
33 kind: crate::readers::Kind::Text,
34 trusted: crate::readers::Trusted {
35 listing: false,
36 ..crate::readers::EVERYWHERE
37 },
38 }],
39 ..crate::readers::BASE
40};
41
42pub const MAX_TOKEN: usize = 1 << 20;
44pub const MAX_TEXT: usize = 4096;
46pub const MAX_DEPTH: usize = 256;
48pub const MAX_VARS: usize = 1 << 20;
50pub const MAX_PATHS: usize = 64 << 20;
52const MAX_SECTION_TOKENS: usize = 16;
54pub const MAX_EXTEND: u32 = 4096;
56pub const BATCH_ROWS: usize = 65_536;
58pub const BATCH_TEXT: usize = 32 << 20;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Scale {
64 Nanos(i64),
66 Count { per_tick: i64, unit: &'static str },
68}
69
70impl Scale {
71 fn dtype(self) -> DataType {
72 match self {
73 Scale::Nanos(_) => DataType::Duration(TimeUnit::Nanoseconds),
74 Scale::Count { .. } => DataType::Int64,
75 }
76 }
77
78 fn per_tick(self) -> i64 {
79 match self {
80 Scale::Nanos(n) => n,
81 Scale::Count { per_tick, .. } => per_tick,
82 }
83 }
84}
85
86pub fn parse_timescale(text: &str) -> Option<Scale> {
88 let text: String = text.split_whitespace().collect();
89 let digits = text.find(|c: char| !c.is_ascii_digit())?;
90 let (number, unit) = text.split_at(digits);
91 let number: i64 = number.parse().ok()?;
92 if !matches!(number, 1 | 10 | 100) {
93 return None;
94 }
95 Some(match unit {
96 "s" => Scale::Nanos(number * 1_000_000_000),
97 "ms" => Scale::Nanos(number * 1_000_000),
98 "us" => Scale::Nanos(number * 1_000),
99 "ns" => Scale::Nanos(number),
100 "ps" => Scale::Count {
101 per_tick: number,
102 unit: "ps",
103 },
104 "fs" => Scale::Count {
105 per_tick: number,
106 unit: "fs",
107 },
108 _ => return None,
109 })
110}
111
112pub fn looks_like(head: &[u8]) -> bool {
115 let text = head.strip_prefix(b"\xef\xbb\xbf").unwrap_or(head);
116 let start = text
117 .iter()
118 .position(|b| !b.is_ascii_whitespace())
119 .unwrap_or(text.len());
120 let text = &text[start..];
121 let first = text
122 .split(|b| b.is_ascii_whitespace())
123 .next()
124 .unwrap_or_default();
125 matches!(
126 first,
127 b"$date" | b"$version" | b"$timescale" | b"$comment" | b"$scope" | b"$var"
128 ) && text.windows(4).any(|w| w == b"$end")
129}
130
131#[derive(Debug, Clone, PartialEq)]
133pub struct Var {
134 pub path: String,
136 pub kind: String,
138 pub width: u32,
139 pub id: String,
141}
142
143#[derive(Debug, Clone, Default, PartialEq)]
145pub struct Header {
146 pub date: Option<String>,
147 pub version: Option<String>,
148 pub timescale: Option<String>,
149 pub comments: Vec<String>,
150 pub scopes: u64,
151 pub vars: Vec<Var>,
152}
153
154#[derive(Debug, Clone, Default, PartialEq)]
156pub struct Stats {
157 pub rows: u64,
159 pub times: u64,
161 pub first_time: Option<i64>,
162 pub last_time: Option<i64>,
163 pub unreadable: u64,
165 pub undeclared: u64,
167 pub vars_dropped: u64,
169 pub overflowed: u64,
171 pub backwards: u64,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176enum Section {
177 Date,
178 Version,
179 Timescale,
180 Comment,
181 Scope,
182 Upscope,
183 Var,
184 EndDefinitions,
185 Other,
186}
187
188#[derive(Debug, Clone, PartialEq)]
190enum Value {
191 Vector(String),
193 Real(String),
195 Text(String),
197 Scalar(char),
199}
200
201#[derive(Debug)]
202enum Pending {
203 Nothing,
204 Section {
205 what: Section,
206 tokens: Vec<String>,
207 text: usize,
208 },
209 Id(Value),
210}
211
212#[derive(Debug, Default)]
214struct Rows {
215 time: Vec<Option<i64>>,
216 var: Vec<u32>,
217 value: Vec<String>,
218 int: Vec<Option<u64>>,
219}
220
221#[derive(Debug)]
223pub struct VcdReader {
224 buf: Vec<u8>,
225 skipping: bool,
227 in_body: bool,
228 pending: Pending,
229 scope: Vec<String>,
230 too_deep: usize,
232 path_bytes: usize,
234 header: Header,
235 ids: HashMap<String, Vec<u32>>,
236 scale: Option<Scale>,
237 time: i64,
238 rows: Rows,
239 held: usize,
240 stats: Stats,
241 seen: bool,
243}
244
245impl Default for VcdReader {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251impl VcdReader {
252 pub fn new() -> Self {
253 Self {
254 buf: Vec::new(),
255 skipping: false,
256 in_body: false,
257 pending: Pending::Nothing,
258 scope: Vec::new(),
259 too_deep: 0,
260 path_bytes: 0,
261 header: Header::default(),
262 ids: HashMap::new(),
263 scale: None,
264 time: 0,
265 rows: Rows::default(),
266 held: 0,
267 stats: Stats::default(),
268 seen: false,
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() >= MAX_VARS
561 || self.too_deep > 0
562 || len > MAX_TEXT
563 || self.path_bytes + len > MAX_PATHS
564 {
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.vars_dropped > 0 {
691 notes.push(note(
692 format!(
693 "{} left out: past limits ({} signals, {} scopes deep, {} bytes a name)",
694 count(stats.vars_dropped, "declaration", "declarations"),
695 group_u64(MAX_VARS as u64),
696 MAX_DEPTH,
697 group_u64(MAX_TEXT as u64)
698 ),
699 "in the header".to_string(),
700 ));
701 }
702 if stats.overflowed > 0 {
703 notes.push(note(
704 format!(
705 "{} past the Duration range {} time null",
706 count(stats.overflowed, "value change", "value changes"),
707 crate::glyphs::get().middot
708 ),
709 of_rows.clone(),
710 ));
711 }
712 if stats.backwards > 0 {
713 notes.push(note(
714 format!(
715 "{} earlier than the one before",
716 count(stats.backwards, "#time", "#times")
717 ),
718 format!("of {}", count(stats.times, "#time", "#times")),
719 ));
720 }
721 match reader.scale() {
722 Some(Scale::Count { unit: "ticks", .. }) => notes.push(note(
723 format!(
724 "no $timescale {} time in ticks",
725 crate::glyphs::get().middot
726 ),
727 "in the header".to_string(),
728 )),
729 Some(Scale::Count { unit, .. }) => notes.push(note(
730 format!(
731 "timescale finer than a Duration {} time in {unit}",
732 crate::glyphs::get().middot
733 ),
734 format!(
735 "from $timescale {}",
736 reader.header().timescale.as_deref().unwrap_or_default()
737 ),
738 )),
739 _ => {}
740 }
741 notes
742}
743
744fn group_u64(n: u64) -> String {
745 crate::numfmt::group_chrome(usize::try_from(n).unwrap_or(usize::MAX))
746}
747
748fn span_text(first: i64, last: i64, scale: Scale) -> String {
751 let (per, units): (i64, &[(&str, i64)]) = match scale {
752 Scale::Nanos(ns) => (
753 ns,
754 &[
755 ("s", 1_000_000_000),
756 ("ms", 1_000_000),
757 ("us", 1_000),
758 ("ns", 1),
759 ],
760 ),
761 Scale::Count { per_tick, unit } => (
762 per_tick,
763 if unit == "ps" {
764 &[("ps", 1)]
765 } else if unit == "fs" {
766 &[("fs", 1)]
767 } else {
768 &[("ticks", 1)]
769 },
770 ),
771 };
772 let (Some(a), Some(b)) = (first.checked_mul(per), last.checked_mul(per)) else {
773 return format!("#{first} to #{last}");
774 };
775 let (unit, div) = units
776 .iter()
777 .find(|(_, d)| a % d == 0 && b % d == 0)
778 .copied()
779 .unwrap_or(("ns", 1));
780 let show = |n: i64| {
781 let text = group_u64((n / div).unsigned_abs());
782 if n < 0 { format!("-{text}") } else { text }
783 };
784 format!("{} to {} {unit}", show(a), show(b))
785}
786
787pub fn detail(reader: &VcdReader) -> Detail {
789 let header = reader.header();
790 let stats = reader.stats();
791 let sep = format!(" {} ", crate::glyphs::get().middot);
792 let mut head = String::from("VCD");
793 if let Some(timescale) = &header.timescale {
794 head.push_str(&sep);
795 head.push_str(&format!("timescale {timescale}"));
796 }
797 head.push_str(&sep);
798 head.push_str(&count(header.vars.len() as u64, "signal", "signals"));
799 head.push_str(&format!(" in {}", count(header.scopes, "scope", "scopes")));
800 let mut lines = vec![head];
801 let mut body = count(stats.rows, "value change", "value changes");
802 if let (Some(first), Some(last), Some(scale)) =
803 (stats.first_time, stats.last_time, reader.scale())
804 {
805 body.push_str(&sep);
806 body.push_str(&span_text(first, last, scale));
807 }
808 lines.push(body);
809 if let Some(date) = header.date.as_deref().filter(|d| !d.is_empty()) {
810 lines.push(format!("Date: {date}"));
811 }
812 if let Some(version) = header.version.as_deref().filter(|v| !v.is_empty()) {
813 lines.push(format!("Version: {version}"));
814 }
815 for comment in header.comments.iter().filter(|c| !c.is_empty()) {
816 lines.push(format!("Comment: {comment}"));
817 }
818 let list = capped_list(
819 header.vars.iter().map(|v| {
820 (
821 v.path.clone(),
822 MetaValue::Text(format!(
823 "{}{sep}{}{sep}id {}",
824 v.kind,
825 count(u64::from(v.width), "bit", "bits"),
826 v.id
827 )),
828 )
829 }),
830 header.vars.len(),
831 );
832 Detail {
833 tab: crate::text_formats::tab(crate::FileFormat::Vcd),
834 lines,
835 list_title: "Signals",
836 list,
837 first: true,
838 ..Default::default()
839 }
840}
841
842pub(crate) fn convert(
845 display: &Path,
846 options: &OpenOptions,
847 writer: &Writer,
848 pieces: &mut Pieces<'_>,
849) -> Result<(Converted, Detail)> {
850 let mut reader = VcdReader::new();
851 let mut segments = Segments::new(options, writer);
852 pieces(&mut |piece| {
853 reader.push(piece);
854 if let Some(df) = reader.take_batch()? {
855 segments.write(&df)?;
856 }
857 Ok(())
858 })?;
859 let last = reader
860 .finish()
861 .map_err(|e| crate::error_display::FileError::new(display, e))?;
862 segments.write(&last)?;
863 let (lf, files) = segments.finish()?;
864 let detail = detail(&reader);
865 Ok((
866 Converted {
867 lf,
868 files,
869 notes: notes(&reader),
870 other_tables: Vec::new(),
871 },
872 detail,
873 ))
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879
880 #[test]
882 fn errors_name_the_file() {
883 crate::readers::bad_input::each_names_its_file(
884 crate::FileFormat::Vcd,
885 &[("words.vcd", b"hello there\n", "Not a VCD file")],
886 );
887 }
888
889 const SAMPLE: &str = "$date Mon Oct 2 2026 $end
890$version Icarus Verilog $end
891$timescale 1ns $end
892$scope module top $end
893$var wire 1 ! clk $end
894$scope module cpu $end
895$var wire 8 \" data [7:0] $end
896$var real 64 # temp $end
897$upscope $end
898$upscope $end
899$enddefinitions $end
900#0
901$dumpvars
9020!
903bx \"
904r20.5 #
905$end
906#5
9071!
908b101 \"
909#10
9100!
911b11111111 \"
912";
913
914 fn read(text: &[u8], piece: usize) -> (DataFrame, VcdReader) {
915 let mut reader = VcdReader::new();
916 let mut frames = Vec::new();
917 for chunk in text.chunks(piece) {
918 reader.push(chunk);
919 if let Some(df) = reader.take_batch().unwrap() {
920 frames.push(df);
921 }
922 }
923 frames.push(reader.finish().unwrap());
924 let mut df = frames.remove(0);
925 for f in frames {
926 df.vstack_mut(&f).unwrap();
927 }
928 (df, reader)
929 }
930
931 fn strings(df: &DataFrame, name: &str) -> Vec<String> {
932 df.column(name)
933 .unwrap()
934 .str()
935 .unwrap()
936 .iter()
937 .map(|s| s.unwrap_or_default().to_string())
938 .collect()
939 }
940
941 #[test]
942 fn a_dump_reads_as_its_value_changes() {
943 for piece in [1, 3, 7, 4096] {
944 let (df, reader) = read(SAMPLE.as_bytes(), piece);
945 assert_eq!(df.height(), 7, "piece {piece}");
946 assert_eq!(
947 strings(&df, "signal"),
948 [
949 "top.clk",
950 "top.cpu.data[7:0]",
951 "top.cpu.temp",
952 "top.clk",
953 "top.cpu.data[7:0]",
954 "top.clk",
955 "top.cpu.data[7:0]"
956 ]
957 );
958 assert_eq!(
959 strings(&df, "value"),
960 ["0", "xxxxxxxx", "20.5", "1", "00000101", "0", "11111111"]
961 );
962 let int: Vec<Option<u64>> = df.column("int").unwrap().u64().unwrap().iter().collect();
963 assert_eq!(
964 int,
965 [Some(0), None, None, Some(1), Some(5), Some(0), Some(255)]
966 );
967 let time = df.column("time").unwrap();
968 assert_eq!(time.dtype(), &DataType::Duration(TimeUnit::Nanoseconds));
969 let ns: Vec<Option<i64>> = time.duration().unwrap().physical().iter().collect();
970 assert_eq!(ns[3], Some(5));
971 assert_eq!(ns[6], Some(10));
972 let width: Vec<Option<u32>> =
973 df.column("width").unwrap().u32().unwrap().iter().collect();
974 assert_eq!(width[1], Some(8));
975 assert_eq!(reader.header().scopes, 2);
976 assert_eq!(reader.header().date.as_deref(), Some("Mon Oct 2 2026"));
977 assert_eq!(reader.stats().unreadable, 0);
978 assert_eq!(df.schema().as_ref(), &reader.schema().unwrap());
979 }
980 }
981
982 #[test]
983 fn timescales() {
984 assert_eq!(parse_timescale("1ns"), Some(Scale::Nanos(1)));
985 assert_eq!(parse_timescale("10 us"), Some(Scale::Nanos(10_000)));
986 assert_eq!(parse_timescale("100 ms"), Some(Scale::Nanos(100_000_000)));
987 assert_eq!(
988 parse_timescale("1ps"),
989 Some(Scale::Count {
990 per_tick: 1,
991 unit: "ps"
992 })
993 );
994 assert_eq!(parse_timescale("3ns"), None);
995 assert_eq!(parse_timescale("1 parsec"), None);
996 }
997
998 #[test]
999 fn a_picosecond_timescale_counts_picoseconds() {
1000 let text = "$timescale 10ps $end $var wire 1 ! a $end $enddefinitions $end #3 1!";
1001 let (df, reader) = read(text.as_bytes(), 5);
1002 let time = df.column("time").unwrap();
1003 assert_eq!(time.dtype(), &DataType::Int64);
1004 assert_eq!(time.i64().unwrap().get(0), Some(30));
1005 assert!(
1006 notes(&reader)
1007 .iter()
1008 .any(|n| n.summary.contains("time in ps"))
1009 );
1010 }
1011
1012 #[test]
1013 fn aliases_undeclared_ids_and_garbage() {
1014 let text = "$timescale 1 us $end
1015$scope module a $end $var wire 1 ! x $end $upscope $end
1016$scope module b $end $var wire 1 ! y $end $upscope $end
1017$enddefinitions $end
1018#1 1! 0? %%% #2 z!";
1019 let (df, reader) = read(text.as_bytes(), 2);
1020 assert_eq!(strings(&df, "signal"), ["a.x", "b.y", "a.x", "b.y"]);
1021 assert_eq!(strings(&df, "value"), ["1", "1", "z", "z"]);
1022 assert_eq!(reader.stats().undeclared, 1);
1023 assert_eq!(reader.stats().unreadable, 1);
1024 let ns: Vec<Option<i64>> = df
1025 .column("time")
1026 .unwrap()
1027 .duration()
1028 .unwrap()
1029 .physical()
1030 .iter()
1031 .collect();
1032 assert_eq!(ns, [Some(1000), Some(1000), Some(2000), Some(2000)]);
1033 }
1034
1035 #[test]
1036 fn a_long_token_is_passed_over_and_a_wide_value_kept_whole() {
1037 let mut text = b"$var wire 100 ! w $end $enddefinitions $end #0 b".to_vec();
1038 text.extend(std::iter::repeat_n(b'1', 100));
1039 text.extend_from_slice(b" ! #1 b");
1040 text.extend(std::iter::repeat_n(b'0', MAX_TOKEN + 10));
1041 text.extend_from_slice(b" ! #2 b1 !");
1042 let (df, reader) = read(&text, 65536);
1043 assert_eq!(df.height(), 2);
1044 assert_eq!(strings(&df, "value")[0].len(), 100);
1045 assert_eq!(df.column("int").unwrap().u64().unwrap().get(0), None);
1046 assert_eq!(df.column("int").unwrap().u64().unwrap().get(1), Some(1));
1047 assert!(reader.stats().unreadable >= 1);
1049 }
1050
1051 #[test]
1052 fn not_vcd_is_an_error() {
1053 let mut reader = VcdReader::new();
1054 reader.push(b"hello world");
1055 assert!(reader.finish().is_err());
1056 assert!(!looks_like(b"$foo $end"));
1057 assert!(looks_like(b"\n$date\n today\n$end\n"));
1058 }
1059
1060 #[test]
1061 fn the_detail_names_the_header_and_signals() {
1062 let (_, reader) = read(SAMPLE.as_bytes(), 4096);
1063 let detail = detail(&reader);
1064 assert_eq!(detail.tab, "VCD");
1065 assert!(
1066 detail.lines[0].contains("timescale 1ns"),
1067 "{:?}",
1068 detail.lines
1069 );
1070 assert!(detail.lines[0].contains("3 signals in 2 scopes"));
1071 assert!(detail.lines[1].contains("0 to 10 ns"), "{:?}", detail.lines);
1072 assert_eq!(span_text(0, 800, Scale::Nanos(1_000_000_000)), "0 to 800 s");
1073 assert_eq!(span_text(5, 2_500, Scale::Nanos(1)), "5 to 2,500 ns");
1074 assert_eq!(
1075 span_text(
1076 4,
1077 8,
1078 Scale::Count {
1079 per_tick: 10,
1080 unit: "ps"
1081 }
1082 ),
1083 "40 to 80 ps"
1084 );
1085 assert_eq!(detail.list.len(), 3);
1086 assert_eq!(detail.list[1].0, "top.cpu.data[7:0]");
1087 }
1088}