Skip to main content

datui_lib/
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;
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
23/// What datui does with a VCD dump: see [`crate::readers`].
24pub(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
42/// The longest token read; a longer one is passed over.
43pub const MAX_TOKEN: usize = 1 << 20;
44/// The most text kept of one header section (`$date`, `$version`, `$comment`).
45pub const MAX_TEXT: usize = 4096;
46/// The deepest scope nesting.
47pub const MAX_DEPTH: usize = 256;
48/// The most signals declared.
49pub const MAX_VARS: usize = 1 << 20;
50/// The most bytes of signal paths held, all signals together.
51pub const MAX_PATHS: usize = 64 << 20;
52/// The most tokens one `$var` or `$scope` section may hold.
53const MAX_SECTION_TOKENS: usize = 16;
54/// The widest vector a short value is extended to: VCD leaves leading zeros out.
55pub const MAX_EXTEND: u32 = 4096;
56/// Rows held before a batch is handed over.
57pub const BATCH_ROWS: usize = 65_536;
58/// Text held before a batch is handed over, whatever its rows.
59pub const BATCH_TEXT: usize = 32 << 20;
60
61/// What the file's `$timescale` makes of a `#time`.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Scale {
64    /// A Duration: each tick is this many nanoseconds.
65    Nanos(i64),
66    /// Finer than a nanosecond, or not given: `time` counts `unit`s, this many a tick.
67    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
86/// `1 ns`, `10ps`, `100 fs`: the timescale's text as a [`Scale`].
87pub 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
112/// Whether the first bytes are a VCD header: a `$` section that VCD writers start with,
113/// closed by `$end`.
114pub 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/// One declared signal.
132#[derive(Debug, Clone, PartialEq)]
133pub struct Var {
134    /// The dotted scope path and name, with its bit range: `top.cpu.data[7:0]`.
135    pub path: String,
136    /// `wire`, `reg`, `integer`, `real`, ...
137    pub kind: String,
138    pub width: u32,
139    /// The identifier code its changes are written with.
140    pub id: String,
141}
142
143/// The file's header.
144#[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/// What reading noticed.
155#[derive(Debug, Clone, Default, PartialEq)]
156pub struct Stats {
157    /// Rows: value changes, one per signal an identifier names.
158    pub rows: u64,
159    /// `#time` markers.
160    pub times: u64,
161    pub first_time: Option<i64>,
162    pub last_time: Option<i64>,
163    /// Tokens that are not VCD, passed over.
164    pub unreadable: u64,
165    /// Value changes for an identifier no `$var` declared.
166    pub undeclared: u64,
167    /// Signals past [`MAX_VARS`], or scopes past [`MAX_DEPTH`], left out.
168    pub vars_dropped: u64,
169    /// Times that do not fit a Duration in nanoseconds, made null.
170    pub overflowed: u64,
171    /// `#time` markers earlier than the one before.
172    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/// A value read, waiting for its identifier.
189#[derive(Debug, Clone, PartialEq)]
190enum Value {
191    /// `b1010`, without the `b`.
192    Vector(String),
193    /// `r1.5`, without the `r`.
194    Real(String),
195    /// `sHello`, a string change some tools write.
196    Text(String),
197    /// A scalar written apart from its identifier: `1 !`.
198    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/// The rows of one batch.
213#[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/// Reads a VCD file a piece at a time.
222#[derive(Debug)]
223pub struct VcdReader {
224    buf: Vec<u8>,
225    /// Passing over a token longer than [`MAX_TOKEN`] until whitespace ends it.
226    skipping: bool,
227    in_body: bool,
228    pending: Pending,
229    scope: Vec<String>,
230    /// Scopes opened past [`MAX_DEPTH`], kept as a count only.
231    too_deep: usize,
232    /// Bytes of signal paths held, bounded by [`MAX_PATHS`].
233    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    /// Whether anything VCD was read: a section or a time.
242    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    /// 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() >= 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
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.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
748/// The span from `first` to `last` ticks as text, in the coarsest unit both are whole
749/// numbers of: `0 to 800 s`, `5 to 12,500 ns`, `40 to 80 ps`.
750fn 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
787/// The VCD tab of the Info panel: the header and the signals.
788pub 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
842/// Read a VCD file, given a piece at a time by `pieces`, into segments written through
843/// `writer`.
844pub(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    /// A file that is not one names itself, in the one shape.
881    #[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        // The long token, then the `!` it left waiting for nothing.
1048        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}