Skip to main content

datui_lib/formats/
vcd.rs

1//! VCD (value change dump) files from HDL simulators and logic analyzers, read into a
2//! long table: one row per value change of each signal.
3//!
4//! A VCD file is whitespace-separated tokens: a header of `$keyword ... $end` sections
5//! (`$timescale`, `$scope`, `$var`), then `#time` markers and value changes. The reader
6//! takes the file a piece at a time and keeps only the token being read, the header
7//! and one batch of rows. Every length is bounded: a token, a header text, the scope
8//! depth and the number of signals.
9
10use 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
19/// What datui does with a VCD dump: see [`crate::formats::readers`].
20pub(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
38/// The longest token read; a longer one is passed over.
39pub const MAX_TOKEN: usize = 1 << 20;
40/// The most text kept of one header section (`$date`, `$version`, `$comment`).
41pub const MAX_TEXT: usize = 4096;
42/// The deepest scope nesting.
43pub const MAX_DEPTH: usize = 256;
44/// The most bytes of signal paths held, all signals together.
45pub const MAX_PATHS: usize = 64 << 20;
46/// The most tokens one `$var` or `$scope` section may hold.
47const MAX_SECTION_TOKENS: usize = 16;
48/// The widest vector a short value is extended to: VCD leaves leading zeros out.
49pub const MAX_EXTEND: u32 = 4096;
50/// Rows held before a batch is handed over.
51pub const BATCH_ROWS: usize = 65_536;
52/// Text held before a batch is handed over, whatever its rows.
53pub const BATCH_TEXT: usize = 32 << 20;
54
55/// What the file's `$timescale` makes of a `#time`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Scale {
58    /// A Duration: each tick is this many nanoseconds.
59    Nanos(i64),
60    /// Finer than a nanosecond, or not given: `time` counts `unit`s, this many a tick.
61    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
80/// `1 ns`, `10ps`, `100 fs`: the timescale's text as a [`Scale`].
81pub 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
106/// Whether the first bytes are a VCD header: a `$` section that VCD writers start with,
107/// closed by `$end`.
108pub 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/// One declared signal.
126#[derive(Debug, Clone, PartialEq)]
127pub struct Var {
128    /// The dotted scope path and name, with its bit range: `top.cpu.data[7:0]`.
129    pub path: String,
130    /// `wire`, `reg`, `integer`, `real`, ...
131    pub kind: String,
132    pub width: u32,
133    /// The identifier code its changes are written with.
134    pub id: String,
135}
136
137/// The file's header.
138#[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/// What reading noticed.
149#[derive(Debug, Clone, Default, PartialEq)]
150pub struct Stats {
151    /// Rows: value changes, one per signal an identifier names.
152    pub rows: u64,
153    /// `#time` markers.
154    pub times: u64,
155    pub first_time: Option<i64>,
156    pub last_time: Option<i64>,
157    /// Tokens that are not VCD, passed over.
158    pub unreadable: u64,
159    /// Value changes for an identifier no `$var` declared.
160    pub undeclared: u64,
161    /// Signals past `limits.vcd_signals`, left out.
162    pub signals_dropped: u64,
163    /// Signals in scopes past [`MAX_DEPTH`], or with paths past [`MAX_TEXT`] or
164    /// [`MAX_PATHS`], left out.
165    pub vars_dropped: u64,
166    /// Times that do not fit a Duration in nanoseconds, made null.
167    pub overflowed: u64,
168    /// `#time` markers earlier than the one before.
169    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/// A value read, waiting for its identifier.
186#[derive(Debug, Clone, PartialEq)]
187enum Value {
188    /// `b1010`, without the `b`.
189    Vector(String),
190    /// `r1.5`, without the `r`.
191    Real(String),
192    /// `sHello`, a string change some tools write.
193    Text(String),
194    /// A scalar written apart from its identifier: `1 !`.
195    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/// The rows of one batch.
210#[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/// Reads a VCD file a piece at a time.
219#[derive(Debug)]
220pub struct VcdReader {
221    buf: Vec<u8>,
222    /// Passing over a token longer than [`MAX_TOKEN`] until whitespace ends it.
223    skipping: bool,
224    in_body: bool,
225    pending: Pending,
226    scope: Vec<String>,
227    /// Scopes opened past [`MAX_DEPTH`], kept as a count only.
228    too_deep: usize,
229    /// Bytes of signal paths held, bounded by [`MAX_PATHS`].
230    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    /// Whether anything VCD was read: a section or a time.
239    seen: bool,
240    /// `limits.vcd_signals`, read once for the file.
241    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    /// What `time` is, once the header is read; `None` before.
281    pub fn scale(&self) -> Option<Scale> {
282        self.scale
283    }
284
285    /// The table's schema, once the header is read.
286    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    /// Read `bytes`, the next piece of the file.
298    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    /// A full batch, once one is held: [`BATCH_ROWS`] rows, or [`BATCH_TEXT`] of text.
333    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    /// The end of the file: the last token, and the rows not yet taken.
341    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                // A stray `$end` closes nothing.
452                "end" => return,
453                // The body's dump commands frame value changes; they hold nothing.
454                "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                // A header section after the header is not read as one.
463                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                // `$scope module top $end`: the name is the last token.
529                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    /// `$var wire 8 # data [7:0] $end`.
552    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
632/// A vector value as wide as its signal: VCD leaves out leading zeros, and a leading
633/// `x` or `z` stands for as many as are missing.
634fn 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
652/// A binary value of 0s and 1s as an integer, when it fits 64 bits.
653fn 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
667/// What the reader noticed, as the dataset's notes.
668fn 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    // Bounds on the file's shape, not on how much of it is read: no setting.
701    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
758/// The span from `first` to `last` ticks as text, in the coarsest unit both are whole
759/// numbers of: `0 to 800 s`, `5 to 12,500 ns`, `40 to 80 ps`.
760fn 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
797/// The VCD tab of the Info panel: the header and the signals.
798pub 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    /// A file that is not one names itself, in the one shape.
872    #[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        // The long token, then the `!` it left waiting for nothing.
1039        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}