fayalite 0.2.0

Hardware Description Language embedded in Rust, using FIRRTL's semantics
Documentation
// SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information
use crate::{
    intern::{Intern, Interned},
    util::DebugAsDisplay,
};
use hashbrown::HashMap;
use std::{cell::RefCell, fmt, num::NonZeroUsize, panic, path::Path};

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceLocation {
    file: Interned<str>,
    line: u32,
    column: u32,
}

impl fmt::Debug for SourceLocation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("SourceLocation")
            .field(&DebugAsDisplay(self))
            .finish()
    }
}

impl fmt::Display for SourceLocation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let Self { file, line, column } = *self;
        write!(f, "{file}:{line}:{column}")
    }
}

#[derive(Copy, Clone, Debug)]
struct FilePattern(&'static str);

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct FilePatternMatch {
    prefix: String,
    suffix: String,
}

impl From<&'_ FilePatternMatch> for FilePatternMatch {
    fn from(value: &'_ FilePatternMatch) -> Self {
        value.clone()
    }
}

impl FilePatternMatch {
    fn generate_file_name(&self, file_name_id: NonZeroUsize) -> String {
        if file_name_id.get() == 1 {
            self.prefix.clone() + &self.suffix
        } else {
            format!("{}-{file_name_id}{}", self.prefix, self.suffix)
        }
    }
}

impl FilePattern {
    const TEMPLATE_CHAR: char = 'X';
    fn matches(self, file_name: &str) -> Option<FilePatternMatch> {
        if self.0.len() != file_name.len() {
            return None;
        }
        let mut prefix = String::with_capacity(file_name.len());
        for (pattern_ch, ch) in self.0.chars().zip(file_name.chars()) {
            if pattern_ch == Self::TEMPLATE_CHAR {
                if !ch.is_ascii_hexdigit() {
                    return None;
                } else {
                    prefix.push(Self::TEMPLATE_CHAR);
                }
            } else if pattern_ch != ch {
                return None;
            } else {
                prefix.push(ch);
            }
        }
        let split_point = self.0.rfind('.').unwrap_or(self.0.len());
        Some(FilePatternMatch {
            suffix: prefix.split_off(split_point),
            prefix,
        })
    }
}

struct NormalizedFileForTestState {
    file_name_id: NonZeroUsize,
    positions_map: HashMap<(u32, u32), u32>,
}

struct NormalizeFilesForTestsState {
    test_position: &'static panic::Location<'static>,
    file_pattern_matches: HashMap<FilePatternMatch, HashMap<String, NormalizedFileForTestState>>,
}

impl NormalizeFilesForTestsState {
    #[track_caller]
    fn new() -> Self {
        Self {
            test_position: panic::Location::caller(),
            file_pattern_matches: HashMap::new(),
        }
    }
}

const FILE_PATTERNS: &[FilePattern] = &[
    FilePattern("module-XXXXXXXXXX.rs"),
    FilePattern("value-struct-XXXXXXXXXX.rs"),
    FilePattern("value-enum-XXXXXXXXXX.rs"),
];

thread_local! {
    static NORMALIZE_FILES_FOR_TESTS: RefCell<Option<NormalizeFilesForTestsState>> = const {
        RefCell::new(None)
    };
}

impl From<&'_ panic::Location<'_>> for SourceLocation {
    fn from(value: &'_ panic::Location<'_>) -> Self {
        let mut file = value.file();
        let mut line = value.line();
        let mut column = value.column();
        let mut file_str = String::new();
        NORMALIZE_FILES_FOR_TESTS.with_borrow_mut(|state| {
            let Some(state) = state else {
                return;
            };
            if file == state.test_position.file() {
                file = "the_test_file.rs";
                line = line
                    .saturating_add(10000)
                    .saturating_sub(state.test_position.line());
                return;
            }
            file = Path::new(file)
                .file_name()
                .and_then(|v| v.to_str())
                .unwrap_or("<no-file-name>");
            for p in FILE_PATTERNS {
                if let Some(m) = p.matches(file) {
                    let map = state.file_pattern_matches.entry_ref(&m).or_default();
                    let len = map.len();
                    let file_state =
                        map.entry_ref(file)
                            .or_insert_with(|| NormalizedFileForTestState {
                                file_name_id: NonZeroUsize::new(len + 1).unwrap(),
                                positions_map: HashMap::new(),
                            });
                    file_str = m.generate_file_name(file_state.file_name_id);
                    file = &file_str;
                    let positions_len = file_state.positions_map.len();
                    line = *file_state
                        .positions_map
                        .entry((line, column))
                        .or_insert((positions_len + 1).try_into().unwrap());
                    column = 1;
                    break;
                }
            }
        });
        Self {
            file: file.intern(),
            line,
            column,
        }
    }
}

#[must_use = "resets value when dropped"]
pub struct NormalizeFilesForTestsScope {
    old_state: Option<NormalizeFilesForTestsState>,
}

impl Drop for NormalizeFilesForTestsScope {
    fn drop(&mut self) {
        NORMALIZE_FILES_FOR_TESTS.set(self.old_state.take());
    }
}

impl SourceLocation {
    #[track_caller]
    pub fn normalize_files_for_tests() -> NormalizeFilesForTestsScope {
        NormalizeFilesForTestsScope {
            old_state: NORMALIZE_FILES_FOR_TESTS.replace(Some(NormalizeFilesForTestsState::new())),
        }
    }
    #[track_caller]
    pub fn caller() -> Self {
        panic::Location::caller().into()
    }
    pub(crate) fn builtin() -> Self {
        Self::new("builtin".intern(), 1, 1)
    }
    pub const fn new(file: Interned<str>, line: u32, column: u32) -> Self {
        Self { file, line, column }
    }
    pub const fn file(self) -> Interned<str> {
        self.file
    }
    pub const fn line(self) -> u32 {
        self.line
    }
    pub const fn column(self) -> u32 {
        self.column
    }
}