tessellate-core 0.3.0

Compiler and deterministic runtime for the Tess rule language
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// A UTF-8 Tess source file.
#[derive(Clone, Debug)]
pub struct SourceFile {
    pub name: Arc<str>,
    pub text: Arc<str>,
    origins: Arc<[SourceOrigin]>,
}

#[derive(Clone, Debug)]
struct SourceOrigin {
    start: usize,
    end: usize,
    name: Arc<str>,
}

impl SourceFile {
    #[must_use]
    pub fn new(name: impl Into<Arc<str>>, text: impl Into<Arc<str>>) -> Self {
        let name = name.into();
        let text = text.into();
        let origins = vec![SourceOrigin {
            start: 0,
            end: text.len(),
            name: name.clone(),
        }];
        Self {
            name,
            text,
            origins: origins.into(),
        }
    }

    /// Combine source files into one span address space while retaining the
    /// original path and line numbers used to render diagnostics.
    #[must_use]
    pub fn bundle(name: impl Into<Arc<str>>, sources: &[Self]) -> (Self, Vec<usize>) {
        let name = name.into();
        let mut text = String::new();
        let mut origins = Vec::with_capacity(sources.len());
        let mut offsets = Vec::with_capacity(sources.len());
        for source in sources {
            if !text.is_empty() && !text.ends_with('\n') {
                text.push('\n');
            }
            let start = text.len();
            offsets.push(start);
            text.push_str(&source.text);
            let end = text.len();
            origins.push(SourceOrigin {
                start,
                end,
                name: source.name.clone(),
            });
        }
        (
            Self {
                name,
                text: Arc::from(text),
                origins: origins.into(),
            },
            offsets,
        )
    }

    fn origin_at(&self, byte: usize) -> Option<&SourceOrigin> {
        self.origins
            .iter()
            .find(|origin| byte >= origin.start && byte < origin.end)
            .or_else(|| self.origins.last().filter(|origin| byte == origin.end))
    }

    /// Return the original path that owns a byte in this source's span space.
    #[must_use]
    pub fn name_at(&self, byte: usize) -> &str {
        self.origin_at(byte)
            .map_or(self.name.as_ref(), |origin| origin.name.as_ref())
    }

    /// Translate a bundled span into the original file's local byte offsets.
    #[must_use]
    pub fn local_span(&self, span: Span) -> Option<Span> {
        let origin = self.origin_at(span.start)?;
        if span.end > origin.end {
            return None;
        }
        Some(Span::new(
            span.start.saturating_sub(origin.start),
            span.end.saturating_sub(origin.start),
        ))
    }

    #[must_use]
    pub fn line_col(&self, byte: usize) -> (usize, usize) {
        let mut byte = byte.min(self.text.len());
        while !self.text.is_char_boundary(byte) {
            byte = byte.saturating_sub(1);
        }
        let origin_start = self.origin_at(byte).map_or(0, |origin| origin.start);
        let prefix = &self.text[origin_start..byte];
        let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
        let column = prefix.rsplit_once('\n').map_or_else(
            || prefix.chars().count() + 1,
            |(_, tail)| tail.chars().count() + 1,
        );
        (line, column)
    }

    #[must_use]
    pub fn line_text(&self, line: usize) -> &str {
        self.text.lines().nth(line.saturating_sub(1)).unwrap_or("")
    }

    /// Return the original line containing a byte in this source's span space.
    #[must_use]
    pub fn line_text_at(&self, byte: usize) -> &str {
        let Some(origin) = self.origin_at(byte) else {
            let (line, _) = self.line_col(byte);
            return self.line_text(line);
        };
        let local = byte.saturating_sub(origin.start);
        self.text[origin.start..origin.end]
            .lines()
            .nth(
                self.text[origin.start..origin.start + local]
                    .bytes()
                    .filter(|byte| *byte == b'\n')
                    .count(),
            )
            .unwrap_or("")
    }
}

/// A half-open byte range in a source file.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Span {
    pub start: usize,
    pub end: usize,
}

impl Span {
    #[must_use]
    pub const fn new(start: usize, end: usize) -> Self {
        Self { start, end }
    }

    #[must_use]
    pub const fn merge(self, other: Self) -> Self {
        Self {
            start: if self.start < other.start {
                self.start
            } else {
                other.start
            },
            end: if self.end > other.end {
                self.end
            } else {
                other.end
            },
        }
    }
}

/// A value paired with the source range that produced it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Spanned<T> {
    pub value: T,
    pub span: Span,
}

impl<T> Spanned<T> {
    #[must_use]
    pub const fn new(value: T, span: Span) -> Self {
        Self { value, span }
    }
}