Skip to main content

libsurfer/translation/
mod.rs

1use std::collections::HashMap;
2#[cfg(not(target_arch = "wasm32"))]
3use std::path::Path;
4use std::sync::Arc;
5use std::sync::mpsc::Sender;
6
7use ecolor::Color32;
8use eyre::Result;
9#[cfg(not(target_arch = "wasm32"))]
10use toml::Table;
11#[cfg(not(target_arch = "wasm32"))]
12use tracing::warn;
13
14mod basic_translators;
15pub mod clock;
16mod color_translators;
17mod enum_translator;
18mod event_translator;
19mod fixed_point;
20mod instruction_translators;
21#[cfg(not(target_arch = "wasm32"))]
22mod mapping_translators;
23pub mod numeric_translators;
24#[cfg(feature = "python")]
25mod python_translators;
26#[cfg(all(not(target_arch = "wasm32"), feature = "wasm_plugins"))]
27pub mod wasm_translator;
28
29pub use basic_translators::*;
30use clock::ClockTranslator;
31pub(crate) use color_translators::ycbcr_to_rgb;
32use event_translator::EventTranslator;
33#[cfg(not(target_arch = "wasm32"))]
34use instruction_decoder::Decoder;
35pub use instruction_translators::*;
36use itertools::Itertools;
37pub use numeric_translators::*;
38use surfer_translation_types::{
39    BasicTranslator, HierFormatResult, NumericRange, SubFieldFlatTranslationResult,
40    TranslatedValue, TranslationPreference, TranslationResult, Translator, ValueKind, ValueRepr,
41    VariableEncoding, VariableInfo, VariableValue,
42};
43
44use crate::config::SurferTheme;
45use crate::translation::enum_translator::EnumTranslator;
46use crate::wave_container::{ScopeId, VarId};
47use crate::{message::Message, wave_container::VariableMeta};
48
49pub type DynTranslator = dyn Translator<VarId, ScopeId, Message>;
50pub type DynBasicTranslator = dyn BasicTranslator<VarId, ScopeId>;
51
52fn integer_numeric_range(num_bits: u32, signed: bool) -> Option<NumericRange> {
53    if num_bits == 0 || num_bits >= f64::MAX_EXP as u32 {
54        return None;
55    }
56    let (min, max) = if signed {
57        let half = 2.0f64.powi((num_bits - 1).cast_signed());
58        (-half, half - 1.0)
59    } else {
60        (0.0, 2.0f64.powi(num_bits.cast_signed()) - 1.0)
61    };
62    // Span must be finite for Y-axis scaling
63    (max - min).is_finite().then_some(NumericRange { min, max })
64}
65
66#[cfg(not(target_arch = "wasm32"))]
67static DECODERS_DIR: &str = "decoders";
68
69#[cfg(not(target_arch = "wasm32"))]
70static MAPPINGS_DIR: &str = "mappings";
71
72fn translate_with_basic(
73    t: &DynBasicTranslator,
74    variable: &VariableMeta,
75    value: &VariableValue,
76) -> Result<TranslationResult> {
77    let (val, kind) = t.basic_translate(variable.num_bits.unwrap_or(0), value);
78    Ok(TranslationResult {
79        val: ValueRepr::String(val),
80        kind,
81        subfields: vec![],
82    })
83}
84
85#[derive(Clone)]
86pub enum AnyTranslator {
87    Full(Arc<DynTranslator>),
88    Basic(Arc<DynBasicTranslator>),
89    #[cfg(feature = "python")]
90    Python(Arc<python_translators::PythonTranslator>),
91}
92
93impl AnyTranslator {
94    #[must_use]
95    pub fn is_basic(&self) -> bool {
96        matches!(self, AnyTranslator::Basic(_))
97    }
98}
99
100impl Translator<VarId, ScopeId, Message> for AnyTranslator {
101    fn name(&self) -> String {
102        match self {
103            AnyTranslator::Full(t) => t.name(),
104            AnyTranslator::Basic(t) => t.name(),
105            #[cfg(feature = "python")]
106            AnyTranslator::Python(t) => t.name(),
107        }
108    }
109
110    fn set_wave_source(&self, wave_source: Option<surfer_translation_types::WaveSource>) {
111        match self {
112            AnyTranslator::Full(translator) => translator.set_wave_source(wave_source),
113            AnyTranslator::Basic(_) => {}
114            #[cfg(feature = "python")]
115            AnyTranslator::Python(_) => {}
116        }
117    }
118
119    fn translate(
120        &self,
121        variable: &VariableMeta,
122        value: &VariableValue,
123    ) -> Result<TranslationResult> {
124        match self {
125            AnyTranslator::Full(t) => t.translate(variable, value),
126            AnyTranslator::Basic(t) => translate_with_basic(&**t, variable, value),
127            #[cfg(feature = "python")]
128            AnyTranslator::Python(t) => translate_with_basic(&**t, variable, value),
129        }
130    }
131
132    fn variable_info(&self, variable: &VariableMeta) -> Result<VariableInfo> {
133        match self {
134            AnyTranslator::Full(t) => t.variable_info(variable),
135            AnyTranslator::Basic(t) => t.variable_info(variable),
136            #[cfg(feature = "python")]
137            #[cfg(target_family = "unix")]
138            AnyTranslator::Python(t) => t.variable_info(variable),
139        }
140    }
141
142    fn translates(&self, variable: &VariableMeta) -> Result<TranslationPreference> {
143        match self {
144            AnyTranslator::Full(t) => t.translates(variable),
145            AnyTranslator::Basic(t) => t.translates(variable),
146            #[cfg(feature = "python")]
147            AnyTranslator::Python(t) => t.translates(variable),
148        }
149    }
150
151    fn reload(&self, sender: Sender<Message>) {
152        match self {
153            AnyTranslator::Full(t) => t.reload(sender),
154            AnyTranslator::Basic(_) => (),
155            #[cfg(feature = "python")]
156            AnyTranslator::Python(_) => (),
157        }
158    }
159
160    fn variable_name_info(
161        &self,
162        variable: &surfer_translation_types::VariableMeta<VarId, ScopeId>,
163    ) -> Option<surfer_translation_types::translator::VariableNameInfo> {
164        match self {
165            AnyTranslator::Full(translator) => translator.variable_name_info(variable),
166            AnyTranslator::Basic(_) => None,
167            #[cfg(feature = "python")]
168            AnyTranslator::Python(_) => None,
169        }
170    }
171
172    fn translate_numeric(&self, variable: &VariableMeta, value: &VariableValue) -> Option<f64> {
173        match self {
174            AnyTranslator::Full(t) => t.translate_numeric(variable, value),
175            AnyTranslator::Basic(t) => {
176                t.basic_translate_numeric(variable.num_bits.unwrap_or(0), value)
177            }
178            #[cfg(feature = "python")]
179            AnyTranslator::Python(t) => {
180                t.basic_translate_numeric(variable.num_bits.unwrap_or(0), value)
181            }
182        }
183    }
184
185    fn numeric_range(&self, variable: &VariableMeta) -> Option<NumericRange> {
186        match self {
187            AnyTranslator::Full(t) => t.numeric_range(variable),
188            AnyTranslator::Basic(t) => t.basic_numeric_range(variable.num_bits.unwrap_or(0)),
189            #[cfg(feature = "python")]
190            AnyTranslator::Python(t) => t.basic_numeric_range(variable.num_bits.unwrap_or(0)),
191        }
192    }
193}
194
195/// Look inside the config directory and inside "$(cwd)/.surfer" for user-defined decoders
196/// To add a new decoder named 'x', add a directory 'x' to the decoders directory
197/// Inside, multiple toml files can be added which will all be used for decoding 'x'
198/// This is useful e.g., for layering RISC-V extensions
199#[cfg(not(target_arch = "wasm32"))]
200fn find_user_decoders() -> Vec<Arc<DynBasicTranslator>> {
201    let mut decoders: Vec<Arc<DynBasicTranslator>> = vec![];
202    if let Some(proj_dirs) = crate::config::PROJECT_DIR.as_ref() {
203        let mut config_decoders = find_user_decoders_at_path(proj_dirs.config_dir());
204        decoders.append(&mut config_decoders);
205    }
206
207    let mut project_decoders = find_user_decoders_at_path(Path::new(crate::config::LOCAL_DIR));
208    decoders.append(&mut project_decoders);
209
210    decoders
211}
212
213/// Look for user defined decoders in path.
214#[cfg(not(target_arch = "wasm32"))]
215fn find_user_decoders_at_path(path: &Path) -> Vec<Arc<DynBasicTranslator>> {
216    use tracing::{error, info};
217
218    let mut decoders: Vec<Arc<DynBasicTranslator>> = vec![];
219    let p = path.join(DECODERS_DIR);
220    info!("Looking for user decoders at {}", p.display());
221    let Ok(decoder_dirs) = std::fs::read_dir(p) else {
222        return decoders;
223    };
224
225    for decoder_dir in decoder_dirs.flatten() {
226        if decoder_dir.metadata().is_ok_and(|m| m.is_dir()) {
227            let Ok(name) = decoder_dir.file_name().into_string() else {
228                warn!("Cannot load decoder. Invalid name.");
229                continue;
230            };
231            let mut tomls = vec![];
232            // Keeps track of the bit width of the first parsed toml
233            // All tomls must use the same width
234            let mut width: Option<toml::Value> = None;
235
236            if let Ok(toml_files) = std::fs::read_dir(decoder_dir.path()) {
237                for toml_file in toml_files.flatten() {
238                    if toml_file
239                        .file_name()
240                        .into_string()
241                        .is_ok_and(|file_name| file_name.ends_with(".toml"))
242                    {
243                        let Ok(text) = std::fs::read_to_string(toml_file.path()) else {
244                            warn!(
245                                "Skipping toml file {}. Cannot read file.",
246                                toml_file.path().display()
247                            );
248                            continue;
249                        };
250
251                        let Ok(toml_parsed) = text.parse::<Table>() else {
252                            warn!(
253                                "Skipping toml file {}. Cannot parse toml.",
254                                toml_file.path().display()
255                            );
256                            continue;
257                        };
258
259                        let Some(toml_width) = toml_parsed.get("width") else {
260                            warn!(
261                                "Skipping toml file {}. Mandatory key 'width' is missing.",
262                                toml_file.path().display()
263                            );
264                            continue;
265                        };
266
267                        if width.clone().is_some_and(|width| width != *toml_width) {
268                            warn!(
269                                "Skipping toml file {}. Bit widths do not match.",
270                                toml_file.path().display()
271                            );
272                            continue;
273                        }
274                        width = Some(toml_width.clone());
275
276                        tomls.push(toml_parsed);
277                    }
278                }
279            }
280
281            if let Some(width) = width.and_then(|width| width.as_integer()) {
282                match Decoder::new_from_table(tomls) {
283                    Ok(decoder) => {
284                        let translator = InstructionTranslator {
285                            name,
286                            decoder,
287                            num_bits: width.unsigned_abs() as u32,
288                        };
289                        info!(
290                            "Loaded {}-bit instruction decoder: {} ",
291                            width.unsigned_abs(),
292                            translator.name(),
293                        );
294                        decoders.push(Arc::new(translator));
295                    }
296                    Err(e) => {
297                        error!("Error while building decoder {name}");
298                        for toml in e {
299                            for error in toml {
300                                error!("{error}");
301                            }
302                        }
303                    }
304                }
305            }
306        }
307    }
308    decoders
309}
310
311/// Look inside the config directory and inside "$(cwd)/.surfer" for user-defined mapping translators
312/// To add a new mapping translator named 'x', add a file 'x' to the mapping translators directory
313#[cfg(not(target_arch = "wasm32"))]
314fn find_user_mapping_translators() -> Vec<Arc<DynBasicTranslator>> {
315    let mut translators: Vec<Arc<DynBasicTranslator>> = vec![];
316    if let Some(proj_dirs) = &*crate::config::PROJECT_DIR {
317        let mut config_decoders = find_user_mapping_translators_at_path(proj_dirs.config_dir());
318        translators.append(&mut config_decoders);
319    }
320
321    let mut project_decoders =
322        find_user_mapping_translators_at_path(Path::new(crate::config::LOCAL_DIR));
323    translators.append(&mut project_decoders);
324
325    translators
326}
327
328/// Look for user defined mapping translators in path.
329#[cfg(not(target_arch = "wasm32"))]
330fn find_user_mapping_translators_at_path(path: &Path) -> Vec<Arc<DynBasicTranslator>> {
331    let mut mapping_translators: Vec<Arc<DynBasicTranslator>> = vec![];
332    let p = path.join(MAPPINGS_DIR);
333    tracing::info!("Looking for user mapping translators at {}", p.display());
334    let Ok(mapping_files) = std::fs::read_dir(p) else {
335        return mapping_translators;
336    };
337
338    use crate::translation::mapping_translators::MappingTranslator;
339    for mapping_file in mapping_files.flatten() {
340        tracing::info!(
341            "Found user mapping translator file: {}",
342            mapping_file.path().display()
343        );
344        let Ok(utf8_path) = camino::Utf8PathBuf::try_from(mapping_file.path()) else {
345            warn!(
346                "Cannot load mapping translator. Path is not valid UTF-8: {}",
347                mapping_file.path().display()
348            );
349            continue;
350        };
351        let translator = MappingTranslator::new_from_file(utf8_path);
352        match translator {
353            Err(e) => {
354                tracing::error!(
355                    "Cannot load mapping translator from file {}: {}",
356                    mapping_file.path().display(),
357                    e
358                );
359            }
360            Ok(translator) => {
361                tracing::info!(
362                    "Loaded {:?}-bit(s) mapping translator: {}",
363                    translator.bits(),
364                    translator.name(),
365                );
366                mapping_translators.push(Arc::new(translator));
367            }
368        }
369    }
370    mapping_translators
371}
372
373#[must_use]
374pub fn all_translators() -> TranslatorList {
375    // WASM does not need mut, non-wasm does so we'll allow it
376    #[allow(unused_mut)]
377    let mut basic_translators: Vec<Arc<DynBasicTranslator>> = vec![
378        Arc::new(BitTranslator {}),
379        Arc::new(HexTranslator {}),
380        Arc::new(OctalTranslator {}),
381        Arc::new(GroupingBinaryTranslator {}),
382        Arc::new(BinaryTranslator {}),
383        Arc::new(ASCIITranslator {}),
384        Arc::new(new_rv32_translator()),
385        Arc::new(new_rv64_translator()),
386        Arc::new(new_mips_translator()),
387        Arc::new(new_la64_translator()),
388        Arc::new(LebTranslator {}),
389        Arc::new(UnsignedTranslator {}),
390        Arc::new(SignedTranslator {}),
391        Arc::new(SinglePrecisionTranslator {}),
392        Arc::new(DoublePrecisionTranslator {}),
393        Arc::new(HalfPrecisionTranslator {}),
394        Arc::new(BFloat16Translator {}),
395        Arc::new(Posit32Translator {}),
396        Arc::new(Posit16Translator {}),
397        Arc::new(Posit8Translator {}),
398        Arc::new(PositQuire8Translator {}),
399        Arc::new(PositQuire16Translator {}),
400        Arc::new(E5M2Translator {}),
401        Arc::new(E4M3Translator {}),
402        Arc::new(NumberOfOnesTranslator {}),
403        Arc::new(LeadingOnesTranslator {}),
404        Arc::new(TrailingOnesTranslator {}),
405        Arc::new(LeadingZerosTranslator {}),
406        Arc::new(TrailingZerosTranslator {}),
407        Arc::new(IdenticalMSBsTranslator {}),
408        #[cfg(feature = "f128")]
409        Arc::new(QuadPrecisionTranslator {}),
410        Arc::new(color_translators::RGBTranslator {}),
411        Arc::new(color_translators::GrayScaleTranslator {}),
412        Arc::new(color_translators::YCbCrTranslator {}),
413    ];
414
415    #[cfg(not(target_arch = "wasm32"))]
416    basic_translators.append(&mut find_user_decoders());
417
418    #[cfg(not(target_arch = "wasm32"))]
419    basic_translators.append(&mut find_user_mapping_translators());
420
421    TranslatorList::new(
422        basic_translators,
423        vec![
424            Arc::new(ClockTranslator::new()),
425            Arc::new(StringTranslator {}),
426            Arc::new(EnumTranslator {}),
427            Arc::new(UnsignedFixedPointTranslator),
428            Arc::new(SignedFixedPointTranslator),
429            Arc::new(EventTranslator {}),
430        ],
431    )
432}
433
434#[derive(Default)]
435pub struct TranslatorList {
436    inner: HashMap<String, AnyTranslator>,
437    #[cfg(feature = "python")]
438    python_translator: Option<(camino::Utf8PathBuf, String, AnyTranslator)>,
439    pub default: String,
440}
441
442impl TranslatorList {
443    #[must_use]
444    pub fn new(basic: Vec<Arc<DynBasicTranslator>>, translators: Vec<Arc<DynTranslator>>) -> Self {
445        Self {
446            default: "Hexadecimal".to_string(),
447            inner: basic
448                .into_iter()
449                .map(|t| (t.name(), AnyTranslator::Basic(t)))
450                .chain(
451                    translators
452                        .into_iter()
453                        .map(|t| (t.name(), AnyTranslator::Full(t))),
454                )
455                .collect(),
456            #[cfg(feature = "python")]
457            python_translator: None,
458        }
459    }
460
461    pub fn all_translator_names(&self) -> Vec<&str> {
462        #[cfg(feature = "python")]
463        let python_name = self
464            .python_translator
465            .as_ref()
466            .map(|(_, name, _)| name.as_str());
467        #[cfg(not(feature = "python"))]
468        let python_name = None;
469        self.inner
470            .keys()
471            .map(String::as_str)
472            .chain(python_name)
473            .collect()
474    }
475
476    #[must_use]
477    pub fn all_translators(&self) -> Vec<&AnyTranslator> {
478        #[cfg(feature = "python")]
479        let python_translator = self.python_translator.as_ref().map(|(_, _, t)| t);
480        #[cfg(not(feature = "python"))]
481        let python_translator = None;
482        self.inner.values().chain(python_translator).collect()
483    }
484
485    #[must_use]
486    pub fn basic_translator_names(&self) -> Vec<&str> {
487        self.inner
488            .iter()
489            .filter_map(|(name, t)| t.is_basic().then_some(name.as_str()))
490            .collect()
491    }
492
493    #[must_use]
494    pub fn get_translator(&self, name: &str) -> &AnyTranslator {
495        #[cfg(feature = "python")]
496        let python_translator = || {
497            self.python_translator
498                .as_ref()
499                .filter(|(_, python_name, _)| python_name == name)
500                .map(|(_, _, t)| t)
501        };
502        #[cfg(not(feature = "python"))]
503        let python_translator = || None;
504        self.inner
505            .get(name)
506            .or_else(python_translator)
507            .unwrap_or_else(|| panic!("No translator called {name}"))
508    }
509
510    #[must_use]
511    pub fn clone_translator(&self, name: &str) -> AnyTranslator {
512        self.get_translator(name).clone()
513    }
514
515    pub fn add_or_replace(&mut self, t: AnyTranslator) {
516        self.inner.insert(t.name(), t);
517    }
518
519    #[must_use]
520    pub fn is_valid_translator(&self, meta: &VariableMeta, candidate: &str) -> bool {
521        self.get_translator(candidate)
522            .translates(meta)
523            .is_ok_and(|preference| preference != TranslationPreference::No)
524    }
525
526    #[cfg(feature = "python")]
527    pub fn load_python_translator(&mut self, filename: camino::Utf8PathBuf) -> Result<()> {
528        tracing::debug!("Reading Python code from disk: {filename}");
529        let code = std::ffi::CString::new(std::fs::read_to_string(&filename)?)?;
530        let mut translators = python_translators::PythonTranslator::new(&code.as_c_str())?;
531        if translators.len() != 1 {
532            eyre::bail!("Only one Python translator per file is supported for now");
533        }
534        let translator = translators.pop().unwrap();
535        self.python_translator = Some((
536            filename,
537            translator.name(),
538            AnyTranslator::Python(Arc::new(translator)),
539        ));
540        Ok(())
541    }
542
543    #[cfg(feature = "python")]
544    pub fn has_python_translator(&self) -> bool {
545        self.python_translator.is_some()
546    }
547
548    #[cfg(feature = "python")]
549    pub fn reload_python_translator(&mut self) -> Result<()> {
550        if let Some((path, _, _)) = self.python_translator.take() {
551            self.load_python_translator(path)?;
552        }
553        Ok(())
554    }
555}
556
557fn format(
558    val: &ValueRepr,
559    kind: ValueKind,
560    subtranslator_name: &String,
561    translators: &TranslatorList,
562    subresults: &[HierFormatResult],
563) -> Option<TranslatedValue> {
564    match val {
565        ValueRepr::Bit(val) => {
566            let AnyTranslator::Basic(subtranslator) =
567                translators.get_translator(subtranslator_name)
568            else {
569                panic!("Subtranslator '{subtranslator_name}' was not a basic translator");
570            };
571
572            Some(TranslatedValue::from_basic_translate(
573                subtranslator.basic_translate(1, &VariableValue::String(val.to_string())),
574            ))
575        }
576        ValueRepr::Bits(bit_count, bits) => {
577            let AnyTranslator::Basic(subtranslator) =
578                translators.get_translator(subtranslator_name)
579            else {
580                panic!("Subtranslator '{subtranslator_name}' was not a basic translator");
581            };
582
583            Some(TranslatedValue::from_basic_translate(
584                subtranslator.basic_translate(*bit_count, &VariableValue::String(bits.clone())),
585            ))
586        }
587        ValueRepr::String(sval) => Some(TranslatedValue {
588            value: sval.clone(),
589            kind,
590        }),
591        ValueRepr::Tuple => Some(TranslatedValue {
592            value: format!(
593                "({})",
594                subresults
595                    .iter()
596                    .map(|v| v.this.as_ref().map_or("-", |t| t.value.as_str()))
597                    .join(", ")
598            ),
599            kind,
600        }),
601        ValueRepr::Struct => Some(TranslatedValue {
602            value: format!(
603                "{{{}}}",
604                subresults
605                    .iter()
606                    .map(|v| {
607                        let n = v.names.join("_");
608                        format!("{n}: {}", v.this.as_ref().map_or("-", |t| t.value.as_str()))
609                    })
610                    .join(", ")
611            ),
612            kind,
613        }),
614        ValueRepr::Array => Some(TranslatedValue {
615            value: format!(
616                "[{}]",
617                subresults
618                    .iter()
619                    .map(|v| v.this.as_ref().map_or("-", |t| t.value.as_str()))
620                    .join(", ")
621            ),
622            kind,
623        }),
624        ValueRepr::NotPresent => None,
625        ValueRepr::Enum { idx, name } => Some(TranslatedValue {
626            value: format!(
627                "{name}{{{}}}",
628                subresults[*idx]
629                    .this
630                    .as_ref()
631                    .map_or("-", |t| t.value.as_str())
632            ),
633            kind,
634        }),
635        ValueRepr::Event => Some(TranslatedValue {
636            value: "Event".to_string(),
637            kind,
638        }),
639    }
640}
641
642#[local_impl::local_impl]
643impl TranslationResultExt for TranslationResult {
644    fn sub_format(
645        &self,
646        formats: &[crate::displayed_item::FieldFormat],
647        translators: &TranslatorList,
648        path_so_far: &[String],
649    ) -> Vec<HierFormatResult> {
650        self.subfields
651            .iter()
652            .map(|res| {
653                let sub_path = path_so_far
654                    .iter()
655                    .chain([&res.name])
656                    .cloned()
657                    .collect::<Vec<_>>();
658
659                let sub = res.result.sub_format(formats, translators, &sub_path);
660
661                // we can consistently fall back to the default here since sub-fields
662                // are never checked for their preferred translator
663                let translator_name = formats
664                    .iter()
665                    .find(|e| e.field == sub_path)
666                    .map(|e| e.format.clone())
667                    .unwrap_or(translators.default.clone());
668                let formatted = format(
669                    &res.result.val,
670                    res.result.kind,
671                    &translator_name,
672                    translators,
673                    &sub,
674                );
675
676                HierFormatResult {
677                    this: formatted,
678                    names: sub_path,
679                    fields: sub,
680                }
681            })
682            .collect::<Vec<_>>()
683    }
684
685    /// Flattens the translation result into path, value pairs
686    fn format_flat(
687        &self,
688        root_format: &Option<String>,
689        formats: &[crate::displayed_item::FieldFormat],
690        translators: &TranslatorList,
691    ) -> Vec<SubFieldFlatTranslationResult> {
692        let sub_result = self.sub_format(formats, translators, &[]);
693
694        // FIXME for consistency we should not fall back to `translators.default` here, but fetch the
695        // preferred translator - but doing that ATM will break if the spade translator is used, since
696        // on the first render the spade translator seems to not have loaded its information yet.
697        let formatted = format(
698            &self.val,
699            self.kind,
700            root_format.as_ref().unwrap_or(&translators.default),
701            translators,
702            &sub_result,
703        );
704
705        let formatted = HierFormatResult {
706            names: vec![],
707            this: formatted,
708            fields: sub_result,
709        };
710        let mut collected = vec![];
711        formatted.collect_into(&mut collected);
712        collected
713    }
714}
715
716#[local_impl::local_impl]
717impl VariableInfoExt for VariableInfo {
718    fn get_subinfo(&self, path: &[String]) -> &VariableInfo {
719        match path {
720            [] => self,
721            [field, rest @ ..] => match self {
722                VariableInfo::Compound { subfields } => subfields
723                    .iter()
724                    .find(|(f, _)| f == field)
725                    .unwrap()
726                    .1
727                    .get_subinfo(rest),
728                VariableInfo::Bits => panic!(),
729                VariableInfo::Bool => panic!(),
730                VariableInfo::Clock => panic!(),
731                VariableInfo::String => panic!(),
732                VariableInfo::Real => panic!(),
733                VariableInfo::Event => panic!(),
734            },
735        }
736    }
737
738    fn has_subpath(&self, path: &[String]) -> bool {
739        match path {
740            [] => true,
741            [field, rest @ ..] => match self {
742                VariableInfo::Compound { subfields } => subfields
743                    .iter()
744                    .find(|&(f, _)| f == field)
745                    .is_some_and(|(_, info)| info.has_subpath(rest)),
746                _ => false,
747            },
748        }
749    }
750}
751
752#[local_impl::local_impl]
753impl ValueKindExt for ValueKind {
754    fn color(&self, user_color: Color32, theme: &SurferTheme) -> Color32 {
755        match self {
756            ValueKind::HighImp => theme.variable_highimp,
757            ValueKind::Undef => theme.variable_undef,
758            ValueKind::DontCare => theme.variable_dontcare,
759            ValueKind::Warn => theme.variable_undef,
760            ValueKind::Custom(custom_color) => *custom_color,
761            ValueKind::Weak => theme.variable_weak,
762            ValueKind::Error => theme.accent_error.background,
763            ValueKind::Normal => user_color,
764            ValueKind::Event => theme.variable_event,
765        }
766    }
767}
768
769pub struct StringTranslator {}
770
771impl Translator<VarId, ScopeId, Message> for StringTranslator {
772    fn name(&self) -> String {
773        "String".to_string()
774    }
775
776    fn translate(
777        &self,
778        _variable: &VariableMeta,
779        value: &VariableValue,
780    ) -> Result<TranslationResult> {
781        match value {
782            VariableValue::BigUint(b) => Ok(TranslationResult {
783                val: ValueRepr::String(format!("ERROR (0x{b:x})")),
784                kind: ValueKind::Warn,
785                subfields: vec![],
786            }),
787            VariableValue::String(s) => Ok(TranslationResult {
788                val: ValueRepr::String((*s).clone()),
789                kind: ValueKind::Normal,
790                subfields: vec![],
791            }),
792        }
793    }
794
795    fn variable_info(&self, _variable: &VariableMeta) -> Result<VariableInfo> {
796        Ok(VariableInfo::String)
797    }
798
799    fn translates(&self, variable: &VariableMeta) -> Result<TranslationPreference> {
800        if variable.encoding == VariableEncoding::String {
801            Ok(TranslationPreference::Prefer)
802        } else {
803            Ok(TranslationPreference::No)
804        }
805    }
806}
807
808fn check_single_wordlength(num_bits: Option<u32>, required: u32) -> Result<TranslationPreference> {
809    if Some(required) == num_bits {
810        Ok(TranslationPreference::Yes)
811    } else {
812        Ok(TranslationPreference::No)
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819
820    #[test]
821    fn check_single_wordlength_exact_match() {
822        let result = check_single_wordlength(Some(32), 32).unwrap();
823        assert_eq!(result, TranslationPreference::Yes);
824    }
825
826    #[test]
827    fn check_single_wordlength_mismatch() {
828        let result = check_single_wordlength(Some(64), 32).unwrap();
829        assert_eq!(result, TranslationPreference::No);
830    }
831
832    #[test]
833    fn check_single_wordlength_none() {
834        let result = check_single_wordlength(None, 32).unwrap();
835        assert_eq!(result, TranslationPreference::No);
836    }
837
838    #[test]
839    fn translator_list_basic_operations() {
840        let translators = all_translators();
841
842        // Check that we have some translators
843        assert!(!translators.all_translator_names().is_empty());
844
845        // Check default translator exists
846        assert!(
847            translators
848                .all_translator_names()
849                .contains(&translators.default.as_str())
850        );
851
852        // Check we can get a translator by name
853        let hex_translator = translators.get_translator("Hexadecimal");
854        assert_eq!(hex_translator.name(), "Hexadecimal");
855
856        // Check basic translator names subset
857        let basic_names = translators.basic_translator_names();
858        assert!(basic_names.contains(&"Hexadecimal"));
859        assert!(basic_names.contains(&"Binary"));
860    }
861
862    #[test]
863    fn variable_info_has_subpath() {
864        use surfer_translation_types::VariableInfo;
865
866        let info = VariableInfo::Compound {
867            subfields: vec![
868                ("field1".to_string(), VariableInfo::Bits),
869                (
870                    "field2".to_string(),
871                    VariableInfo::Compound {
872                        subfields: vec![("nested".to_string(), VariableInfo::Bool)],
873                    },
874                ),
875            ],
876        };
877
878        assert!(info.has_subpath(&[]));
879        assert!(info.has_subpath(&["field1".to_string()]));
880        assert!(info.has_subpath(&["field2".to_string()]));
881        assert!(info.has_subpath(&["field2".to_string(), "nested".to_string()]));
882        assert!(!info.has_subpath(&["nonexistent".to_string()]));
883        assert!(!info.has_subpath(&["field2".to_string(), "nonexistent".to_string()]));
884    }
885}