Skip to main content

libsurfer/
fst_export.rs

1//! Exporting the currently displayed variables to an FST waveform file.
2//!
3//! Only the displayed variables (and only the hierarchy needed to reach them, not the full
4//! design hierarchy) are written out.
5
6use std::cmp::Reverse;
7use std::collections::{BTreeMap, BinaryHeap};
8use std::sync::Arc;
9
10use ahash::{AHashMap, AHashSet};
11use camino::{Utf8Path, Utf8PathBuf};
12use eyre::Result;
13use fst_writer::{
14    FstBodyWriter, FstFileType, FstHeaderWriter, FstInfo, FstScopeType, FstSignalId, FstSignalType,
15    FstVarDirection, FstVarType, open_fst,
16};
17use rfd::FileHandle;
18use tracing::{error, warn};
19use wellen::{
20    Hierarchy, ScopeRef, ScopeType, Signal, SignalEncoding, SignalRef, SignalValueRef, Time,
21    TimeTable, TimeTableIdx, Timescale, TimescaleUnit, VarDirection, VarRef, VarType,
22};
23
24use crate::SystemState;
25use crate::async_util::{AsyncJob, perform_async_work};
26use crate::channels::checked_send_many;
27use crate::displayed_item::DisplayedItem;
28use crate::file_dialog::FST_EXPORT_FILTER;
29use crate::message::Message;
30use crate::state_file_io::{sanitize_file_stem, source_file_stem};
31use crate::time::{TimeScale, TimeUnit};
32use crate::wave_container::VariableRef;
33use crate::wellen::WellenContainer;
34
35impl SystemState {
36    /// Variables backing the currently displayed variables, in display order and without
37    /// duplicates.
38    fn displayed_variables(&self) -> Vec<VariableRef> {
39        let Some(waves) = self.user.waves.as_ref() else {
40            return vec![];
41        };
42        waves
43            .items_tree
44            .iter()
45            .filter_map(|node| match waves.displayed_items.get(&node.item_ref) {
46                Some(DisplayedItem::Variable(v)) => Some(v.variable_ref.clone()),
47                _ => None,
48            })
49            .collect()
50    }
51
52    /// Builds the suggested FST export file name, based on the loaded wave source's
53    /// name when available.
54    fn default_fst_export_file_name(&self) -> String {
55        let stem = self
56            .user
57            .waves
58            .as_ref()
59            .and_then(|waves| source_file_stem(&waves.source))
60            .map(|stem| sanitize_file_stem(stem, "export"));
61        match stem {
62            Some(stem) => format!("{stem}_export.fst"),
63            None => "export.fst".to_string(),
64        }
65    }
66
67    /// Exports the currently displayed variables to an FST file.
68    ///
69    /// When `path` is `None`, this opens a save dialog with a suggested filename.
70    pub(crate) fn export_signals_to_fst(&mut self, path: Option<Utf8PathBuf>) {
71        let variables = self.displayed_variables();
72        if variables.is_empty() {
73            error!("No displayed variables to export to FST");
74            return;
75        }
76        let Some(waves) = self.user.waves.as_ref() else {
77            return;
78        };
79        let export = match waves
80            .inner
81            .as_waves()
82            .ok_or_else(|| eyre::eyre!("No waveform data to export"))
83            .and_then(|w| w.prepare_fst_export(&variables))
84        {
85            Ok(export) => export,
86            Err(e) => {
87                error!("Failed to prepare FST export: {e:#?}");
88                return;
89            }
90        };
91
92        let messages = move |destination: FileHandle| async move {
93            let result = match Utf8Path::from_path(destination.path()) {
94                Some(path) => export.write_to_file(path),
95                None => Err(eyre::eyre!(
96                    "File path '{}' contains invalid UTF-8",
97                    destination.path().display()
98                )),
99            };
100            match result {
101                Ok(()) => vec![Message::AsyncDone(AsyncJob::ExportFst)],
102                Err(e) => {
103                    error!("Failed to export variables to FST: {e:#?}");
104                    vec![Message::AsyncDone(AsyncJob::ExportFst)]
105                }
106            }
107        };
108
109        if let Some(path) = path {
110            let sender = self.channels.msg_sender.clone();
111            perform_async_work(async move {
112                checked_send_many(&sender, messages(path.into_std_path_buf().into()).await);
113            });
114        } else {
115            self.file_dialog_save(
116                "Export variables to FST",
117                &FST_EXPORT_FILTER,
118                Some(self.default_fst_export_file_name()),
119                messages,
120            );
121        }
122    }
123}
124
125/// Backend-agnostic, owned snapshot of the data needed to write signals out to an FST file.
126pub(crate) enum FstExport {
127    Wellen(FstExportData),
128}
129
130impl FstExport {
131    /// Writes the previously selected variables to an FST file at `path`.
132    pub(crate) fn write_to_file(&self, path: &Utf8Path) -> Result<()> {
133        match self {
134            FstExport::Wellen(data) => data.write_to_file(path),
135        }
136    }
137}
138
139/// A single node of the minimal scope tree needed to export a set of variables to FST.
140///
141/// Only scopes that lie on the path to an exported variable are included, and only the
142/// exported variables themselves are attached, not their siblings.
143#[derive(Default)]
144pub(crate) struct FstExportNode {
145    /// Wellen scope id, if this node corresponds to an actual scope in the hierarchy.
146    pub(crate) id: Option<ScopeRef>,
147    /// Child scopes, keyed by name.
148    pub(crate) children: BTreeMap<String, FstExportNode>,
149    /// Variables exported directly under this scope.
150    pub(crate) vars: Vec<VarRef>,
151}
152
153/// A self-contained snapshot of everything needed to write a set of variables (and only
154/// the hierarchy needed to reach them) to an FST file.
155///
156/// Kept separate from `WellenContainer` so that the actual (potentially slow) file
157/// writing can be handed off to a background task without holding a borrow of it.
158pub(crate) struct FstExportData {
159    /// Full source hierarchy, needed to resolve scope/var metadata when writing.
160    pub(crate) hierarchy: Arc<Hierarchy>,
161    /// Full source time table; only entries referenced by the exported signals are used.
162    pub(crate) time_table: Arc<TimeTable>,
163    /// Minimal scope tree containing only the exported variables.
164    pub(crate) root: FstExportNode,
165    /// Only the signals that are actually referenced by `root`.
166    pub(crate) signals: AHashMap<SignalRef, Arc<Signal>>,
167    /// FST header version string, copied from the source hierarchy.
168    pub(crate) version: String,
169    /// FST header date string, copied from the source hierarchy.
170    pub(crate) date: String,
171    /// Final FST timescale exponent: the source unit's exponent plus the multiplier's
172    /// and time stamps' folded-in digits.
173    pub(crate) timescale_exponent: i8,
174    /// `10.pow(time_scale_shift)`, precomputed so [`scale_time`] doesn't need to redo it
175    /// for every time stamp. See [`common_power_of_ten`].
176    pub(crate) time_scale_divisor: u128,
177    /// Scaled first time stamp of the source simulation, written into the FST header.
178    pub(crate) start_time: Time,
179    /// The last time stamp of the source simulation, so the export can cover the same
180    /// time span even if no exported signal changes at that point.
181    pub(crate) end_time: Time,
182}
183
184impl FstExportData {
185    /// Writes the previously selected variables to an FST file at `path`.
186    pub(crate) fn write_to_file(&self, path: &Utf8Path) -> Result<()> {
187        let h: &Hierarchy = &self.hierarchy;
188        let info = FstInfo {
189            start_time: self.start_time,
190            timescale_exponent: self.timescale_exponent,
191            version: self.version.clone(),
192            date: self.date.clone(),
193            file_type: FstFileType::VerilogVhdl,
194        };
195
196        let mut header = open_fst(path, &info)?;
197        let mut signal_ids: AHashMap<SignalRef, FstSignalId> = AHashMap::new();
198        write_fst_export_node(h, &mut header, &self.root, &mut signal_ids)?;
199        let mut body = header.finish()?;
200
201        // Collect the actual signal data (deduplicated) so that changes can be merged in time order.
202        let signals = signal_ids
203            .iter()
204            .map(|(signal_ref, fst_id)| (*fst_id, Arc::clone(&self.signals[signal_ref])))
205            .collect::<Vec<_>>();
206
207        self.write_fst_value_changes(&mut body, &signals)?;
208        body.finish()?;
209        Ok(())
210    }
211
212    /// Merges the value changes of `signals` in time order and writes them to `out`, rescaling
213    /// each time stamp with [`scale_time`] (see `time_scale_divisor`).
214    fn write_fst_value_changes<W: std::io::Write + std::io::Seek>(
215        &self,
216        out: &mut FstBodyWriter<W>,
217        signals: &[(FstSignalId, Arc<Signal>)],
218    ) -> Result<()> {
219        let mut iters = signals
220            .iter()
221            .map(|(fst_id, signal)| (*fst_id, signal.iter_changes().peekable()))
222            .collect::<Vec<_>>();
223
224        let mut heap = BinaryHeap::new();
225        for (i, (_, it)) in iters.iter_mut().enumerate() {
226            if let Some((t, _)) = it.peek() {
227                heap.push(Reverse((*t, i)));
228            }
229        }
230
231        let mut last_time_idx: Option<TimeTableIdx> = None;
232        while let Some(Reverse((time_idx, i))) = heap.pop() {
233            if last_time_idx != Some(time_idx) {
234                let time = self.time_table.get(time_idx as usize).copied().unwrap_or(0);
235                out.time_change(scale_time(time, self.time_scale_divisor))?;
236                last_time_idx = Some(time_idx);
237            }
238
239            let (fst_id, it) = &mut iters[i];
240            let (_, value) = it.next().expect("time index was just peeked");
241            match value {
242                SignalValueRef::Event => out.signal_change(*fst_id, &[])?,
243                SignalValueRef::BitVec(bv) => {
244                    out.signal_change(*fst_id, bv.bit_string().as_bytes())?;
245                }
246                SignalValueRef::Real(v) => out.signal_change(*fst_id, &v.to_le_bytes())?,
247                SignalValueRef::String(_) => {
248                    // variable-length strings are not declared during export, so this should not occur
249                }
250            }
251
252            if let Some((next_idx, _)) = it.peek() {
253                heap.push(Reverse((*next_idx, i)));
254            }
255        }
256
257        // Emit a final time change even if no exported signal changes there, so the export
258        // covers the same time span as the source.
259        let last_idx = self
260            .time_table
261            .len()
262            .checked_sub(1)
263            .map(|idx| idx as TimeTableIdx);
264        if !self.time_table.is_empty() && last_time_idx != last_idx {
265            out.time_change(self.end_time)?;
266        }
267        Ok(())
268    }
269}
270
271impl WellenContainer {
272    /// Prepares an owned snapshot sufficient to export `variables` (and only the scopes
273    /// needed to reach them) to an FST file.
274    pub(crate) fn prepare_fst_export(&self, variables: &[VariableRef]) -> Result<FstExportData> {
275        let h: &Hierarchy = &self.hierarchy;
276
277        let mut root = FstExportNode::default();
278        let mut seen = AHashSet::new();
279        for variable in variables {
280            let var_ref = self.get_var_ref(variable)?;
281            if !seen.insert(var_ref) {
282                continue;
283            }
284            let mut node = &mut root;
285            let mut prefix: Vec<String> = Vec::new();
286            for scope in &variable.path.strs {
287                prefix.push(scope.clone());
288                node = node.children.entry(scope.clone()).or_default();
289                if node.id.is_none() {
290                    node.id = h.lookup_scope(&prefix);
291                }
292            }
293            node.vars.push(var_ref);
294        }
295        if seen.is_empty() {
296            eyre::bail!("No variables selected for FST export");
297        }
298
299        let mut signals = AHashMap::with_capacity(seen.len());
300        for var_ref in &seen {
301            let signal_ref = h[*var_ref].signal_ref();
302            if signals.contains_key(&signal_ref) {
303                continue;
304            }
305            let signal = self.signals.get(&signal_ref).cloned().ok_or_else(|| {
306                eyre::eyre!(
307                    "Signal for variable '{}' is not loaded",
308                    h[*var_ref].name(h)
309                )
310            })?;
311            signals.insert(signal_ref, signal);
312        }
313
314        let timescale = h
315            .timescale()
316            .unwrap_or(Timescale::new(1, TimescaleUnit::Unknown));
317        let base_exponent = timescale.unit.to_exponent().unwrap_or(0);
318        // The declared multiplier (e.g. the "10" in "10 ns") is restricted by the VCD/FST
319        // spec to 1, 10 or 100, so its digits can be folded directly into the exponent
320        // without ever needing to multiply the time stamps by a remainder.
321        let factor_digits = u32::from(
322            TimeScale {
323                unit: TimeUnit::from(timescale.unit),
324                multiplier: Some(timescale.factor),
325            }
326            .multiplier_digits(),
327        );
328        let time_scale_shift = common_power_of_ten(&self.time_table);
329        let time_scale_divisor = 10u128.pow(time_scale_shift);
330        let timescale_exponent = base_exponent
331            .saturating_add(factor_digits as i8)
332            .saturating_add(time_scale_shift as i8);
333        let start_time = scale_time(
334            self.time_table.first().copied().unwrap_or(0),
335            time_scale_divisor,
336        );
337        let end_time = scale_time(
338            self.time_table.last().copied().unwrap_or(0),
339            time_scale_divisor,
340        );
341        Ok(FstExportData {
342            hierarchy: Arc::clone(&self.hierarchy),
343            time_table: Arc::clone(&self.time_table),
344            root,
345            signals,
346            version: h.version().to_string(),
347            date: h.date().to_string(),
348            timescale_exponent,
349            time_scale_divisor,
350            start_time,
351            end_time,
352        })
353    }
354}
355
356/// Counts how many trailing (decimal) zeros `n` has, i.e. the largest `k` for which
357/// `10^k` divides `n` evenly. `n` must be non-zero: 0 is a multiple of every power of
358/// ten, which would loop forever. Callers filter out zero time stamps beforehand.
359fn trailing_zeros_base10(mut n: u128) -> u32 {
360    debug_assert_ne!(n, 0, "trailing_zeros_base10(0) would loop forever");
361    let mut count = 0;
362    while n.is_multiple_of(10) {
363        n /= 10;
364        count += 1;
365    }
366    count
367}
368
369/// Finds the largest power of ten that evenly divides every non-zero time stamp `t` in
370/// `times`. This tells us how much coarser than the source's declared timescale the
371/// export's timescale exponent can be (i.e. closer to zero) while still representing
372/// every required time stamp as an exact integer.
373fn common_power_of_ten(times: &[Time]) -> u32 {
374    let mut common = u32::MAX;
375    for &t in times {
376        if t == 0 {
377            continue;
378        }
379        let zeros = trailing_zeros_base10(u128::from(t));
380        common = common.min(zeros);
381        if common == 0 {
382            return 0;
383        }
384    }
385    if common == u32::MAX { 0 } else { common }
386}
387
388/// Rescales a raw time stamp from the source's timescale to the export's timescale,
389/// given the precomputed divisor `10.pow(time_scale_shift)` (see [`common_power_of_ten`]).
390fn scale_time(t: Time, divisor: u128) -> Time {
391    (u128::from(t) / divisor) as Time
392}
393
394/// Recursively declares `node`'s scopes and variables in the FST header.
395fn write_fst_export_node<W: std::io::Write + std::io::Seek>(
396    h: &Hierarchy,
397    out: &mut FstHeaderWriter<W>,
398    node: &FstExportNode,
399    signal_ids: &mut AHashMap<SignalRef, FstSignalId>,
400) -> Result<()> {
401    for var_ref in &node.vars {
402        write_fst_export_var(h, out, signal_ids, *var_ref)?;
403    }
404    for (name, child) in &node.children {
405        let (component, tpe) = match child.id {
406            Some(id) => {
407                let scope = &h[id];
408                (
409                    scope.component(h).unwrap_or("").to_string(),
410                    fst_scope_type(scope.scope_type()),
411                )
412            }
413            None => (String::new(), FstScopeType::Module),
414        };
415        out.scope(name, &component, tpe)?;
416        write_fst_export_node(h, out, child, signal_ids)?;
417        out.up_scope()?;
418    }
419    Ok(())
420}
421
422/// Declares a single variable in the FST header, reusing an existing signal id if the
423/// same signal was already declared under a different variable name (aliasing).
424fn write_fst_export_var<W: std::io::Write + std::io::Seek>(
425    h: &Hierarchy,
426    out: &mut FstHeaderWriter<W>,
427    signal_ids: &mut AHashMap<SignalRef, FstSignalId>,
428    var_ref: VarRef,
429) -> Result<()> {
430    let var = &h[var_ref];
431    let name = var.name(h);
432    let signal_tpe = match var.signal_encoding(h) {
433        SignalEncoding::String => {
434            warn!(
435                "Variable-length string signal '{name}' is not supported by FST export, skipping it"
436            );
437            return Ok(());
438        }
439        SignalEncoding::Real => FstSignalType::real(),
440        SignalEncoding::BitVector(len) => FstSignalType::bit_vec(len),
441    };
442    let tpe = fst_var_type(var.var_type());
443    let dir = fst_var_direction(var.direction());
444    let signal_ref = var.signal_ref();
445    let alias = signal_ids.get(&signal_ref).copied();
446    let fst_id = out.var(name, signal_tpe, tpe, dir, alias)?;
447    signal_ids.entry(signal_ref).or_insert(fst_id);
448    Ok(())
449}
450
451/// Maps a wellen scope type to its FST equivalent.
452fn fst_scope_type(tpe: ScopeType) -> FstScopeType {
453    match tpe {
454        ScopeType::Module => FstScopeType::Module,
455        ScopeType::Task => FstScopeType::Task,
456        ScopeType::Function | ScopeType::VhdlFunction => FstScopeType::Function,
457        ScopeType::Begin => FstScopeType::Begin,
458        ScopeType::Fork => FstScopeType::Fork,
459        ScopeType::Generate => FstScopeType::Generate,
460        ScopeType::Struct | ScopeType::VhdlRecord => FstScopeType::Struct,
461        ScopeType::Union => FstScopeType::Union,
462        ScopeType::Class => FstScopeType::Class,
463        ScopeType::Interface => FstScopeType::Interface,
464        ScopeType::Package | ScopeType::VhdlPackage => FstScopeType::Package,
465        ScopeType::Program => FstScopeType::Program,
466        ScopeType::VhdlArchitecture => FstScopeType::VhdlArchitecture,
467        ScopeType::VhdlProcedure => FstScopeType::VhdlProcedure,
468        ScopeType::VhdlProcess => FstScopeType::VhdlProcess,
469        ScopeType::VhdlBlock => FstScopeType::VhdlBlock,
470        ScopeType::VhdlForGenerate => FstScopeType::VhdlForGenerate,
471        ScopeType::VhdlIfGenerate => FstScopeType::VhdlIfGenerate,
472        ScopeType::VhdlGenerate | ScopeType::GhwGeneric => FstScopeType::VhdlGenerate,
473        ScopeType::VhdlArray | ScopeType::SvArray => FstScopeType::Struct,
474        _ => FstScopeType::Module,
475    }
476}
477
478/// Maps a wellen variable type to its FST equivalent.
479fn fst_var_type(tpe: VarType) -> FstVarType {
480    match tpe {
481        VarType::Event | VarType::EventParameter => FstVarType::Event,
482        VarType::Integer => FstVarType::Integer,
483        VarType::Parameter => FstVarType::Parameter,
484        VarType::Real | VarType::RealParameter => FstVarType::Real,
485        VarType::Reg => FstVarType::Reg,
486        VarType::Supply0 => FstVarType::Supply0,
487        VarType::Supply1 => FstVarType::Supply1,
488        VarType::Time => FstVarType::Time,
489        VarType::Tri => FstVarType::Tri,
490        VarType::TriAnd => FstVarType::TriAnd,
491        VarType::TriOr => FstVarType::TriOr,
492        VarType::TriReg => FstVarType::TriReg,
493        VarType::Tri0 => FstVarType::Tri0,
494        VarType::Tri1 => FstVarType::Tri1,
495        VarType::WAnd => FstVarType::Wand,
496        VarType::Wire => FstVarType::Wire,
497        VarType::WOr => FstVarType::Wor,
498        VarType::String => FstVarType::GenericString,
499        VarType::Port => FstVarType::Port,
500        VarType::SparseArray => FstVarType::SparseArray,
501        VarType::RealTime => FstVarType::RealTime,
502        VarType::Bit | VarType::Boolean | VarType::StdLogic | VarType::StdULogic => FstVarType::Bit,
503        VarType::Logic
504        | VarType::BitVector
505        | VarType::StdLogicVector
506        | VarType::StdULogicVector => FstVarType::Logic,
507        VarType::Int => FstVarType::Int,
508        VarType::ShortInt => FstVarType::ShortInt,
509        VarType::LongInt => FstVarType::LongInt,
510        VarType::Byte => FstVarType::Byte,
511        VarType::Enum => FstVarType::Enum,
512        VarType::ShortReal => FstVarType::ShortReal,
513    }
514}
515
516/// Maps a wellen variable direction to its FST equivalent.
517fn fst_var_direction(dir: VarDirection) -> FstVarDirection {
518    match dir {
519        VarDirection::Unknown | VarDirection::Implicit => FstVarDirection::Implicit,
520        VarDirection::Input => FstVarDirection::Input,
521        VarDirection::Output => FstVarDirection::Output,
522        VarDirection::InOut => FstVarDirection::InOut,
523        VarDirection::Buffer => FstVarDirection::Buffer,
524        VarDirection::Linkage => FstVarDirection::Linkage,
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn trailing_zeros_base10_counts_correctly() {
534        assert_eq!(trailing_zeros_base10(1), 0);
535        assert_eq!(trailing_zeros_base10(7), 0);
536        assert_eq!(trailing_zeros_base10(10), 1);
537        assert_eq!(trailing_zeros_base10(120), 1);
538        assert_eq!(trailing_zeros_base10(100), 2);
539        assert_eq!(trailing_zeros_base10(123_000), 3);
540        assert_eq!(trailing_zeros_base10(1_000_000_000_000), 12);
541    }
542
543    #[test]
544    fn common_power_of_ten_of_empty_or_all_zero_is_zero() {
545        assert_eq!(common_power_of_ten(&[]), 0);
546        assert_eq!(common_power_of_ten(&[0, 0, 0]), 0);
547    }
548
549    #[test]
550    fn common_power_of_ten_ignores_zero_time_stamps() {
551        // The zero time stamp shouldn't constrain the result.
552        assert_eq!(common_power_of_ten(&[0, 100, 200]), 2);
553    }
554
555    #[test]
556    fn common_power_of_ten_uses_the_smallest_shared_shift() {
557        assert_eq!(common_power_of_ten(&[10, 20, 30]), 1);
558        assert_eq!(common_power_of_ten(&[100, 200, 300]), 2);
559        // one non-round time stamp caps the shift for all of them
560        assert_eq!(common_power_of_ten(&[100, 200, 305]), 0);
561    }
562
563    #[test]
564    fn common_power_of_ten_short_circuits_at_zero() {
565        assert_eq!(common_power_of_ten(&[1, 100, 1_000_000]), 0);
566    }
567
568    #[test]
569    fn scale_time_divides_by_the_given_divisor() {
570        assert_eq!(scale_time(1_234, 1), 1_234);
571        assert_eq!(scale_time(1_230, 10), 123);
572        assert_eq!(scale_time(1_200_000, 100), 12_000);
573        assert_eq!(scale_time(0, 1000), 0);
574    }
575
576    #[test]
577    fn fst_scope_type_maps_common_variants() {
578        assert_eq!(fst_scope_type(ScopeType::Module), FstScopeType::Module);
579        assert_eq!(fst_scope_type(ScopeType::Struct), FstScopeType::Struct);
580        assert_eq!(fst_scope_type(ScopeType::VhdlRecord), FstScopeType::Struct);
581        assert_eq!(fst_scope_type(ScopeType::VhdlArray), FstScopeType::Struct);
582        assert_eq!(fst_scope_type(ScopeType::Unknown), FstScopeType::Module);
583    }
584
585    #[test]
586    fn fst_var_type_maps_common_variants() {
587        assert_eq!(fst_var_type(VarType::Wire), FstVarType::Wire);
588        assert_eq!(fst_var_type(VarType::Reg), FstVarType::Reg);
589        assert_eq!(fst_var_type(VarType::Event), FstVarType::Event);
590        assert_eq!(fst_var_type(VarType::EventParameter), FstVarType::Event);
591        assert_eq!(fst_var_type(VarType::Bit), FstVarType::Bit);
592        assert_eq!(fst_var_type(VarType::Logic), FstVarType::Logic);
593        assert_eq!(fst_var_type(VarType::BitVector), FstVarType::Logic);
594    }
595
596    #[test]
597    fn fst_var_direction_maps_common_variants() {
598        assert_eq!(
599            fst_var_direction(VarDirection::Unknown),
600            FstVarDirection::Implicit
601        );
602        assert_eq!(
603            fst_var_direction(VarDirection::Implicit),
604            FstVarDirection::Implicit
605        );
606        assert_eq!(
607            fst_var_direction(VarDirection::Input),
608            FstVarDirection::Input
609        );
610        assert_eq!(
611            fst_var_direction(VarDirection::Output),
612            FstVarDirection::Output
613        );
614        assert_eq!(
615            fst_var_direction(VarDirection::InOut),
616            FstVarDirection::InOut
617        );
618    }
619}