hamelin_lib 0.21.6

Core library for Hamelin query language
Documentation
use std::sync::Arc;
use std::{error::Error, ops::RangeInclusive};

use crate::{
    err::{
        Context, DiagnosticSink, LanguageArea, Level, Stage, TranslationError, TranslationErrors,
    },
    interner::Interner,
    tree::ast::node::Spannable,
    types::Type,
};

/// Context needed for parsing CST to AST
///
/// This context contains a diagnostic sink for accumulating parse errors
/// as the AST is constructed from the CST.
#[derive(Debug)]
pub struct ParseContext {
    /// Diagnostic sink - accumulates errors during AST construction
    /// Stores Arc<TranslationError> for efficient sharing without cloning
    errors: Vec<Arc<TranslationError>>,

    /// String interner for deduplicating identifier strings.
    /// All identifier strings go through the interner for memory savings.
    /// A default interner is created automatically, or a shared one can be provided
    /// via `with_interner()` to deduplicate across multiple parse operations.
    interner: Arc<Interner>,
}

impl ParseContext {
    /// Create a new parse context with a default interner.
    pub fn new() -> Self {
        Self {
            errors: Vec::new(),
            interner: Arc::new(Interner::new()),
        }
    }

    /// Create a new parse context with a shared string interner.
    ///
    /// Use this to share an interner across multiple parse operations,
    /// which deduplicates identifier strings across all of them.
    pub fn with_interner(interner: Arc<Interner>) -> Self {
        Self {
            errors: Vec::new(),
            interner,
        }
    }

    /// Intern a string, returning a shared reference.
    ///
    /// Identical strings within this parse context (or across contexts sharing
    /// the same interner) will return the same `Arc<str>`.
    pub fn intern(&self, s: &str) -> Arc<str> {
        self.interner.intern(s)
    }

    /// Intern a type, returning a shared reference.
    ///
    /// Structurally identical types will return the same `Arc<Type>`.
    pub fn intern_type(&self, t: Type) -> Arc<Type> {
        self.interner.intern_type(t)
    }

    /// Create an error builder for constructing errors with this context
    pub fn error(&mut self, message: impl Into<String>) -> ErrorBuilder<'_, Self> {
        ErrorBuilder::new(message.into(), self)
    }

    /// Add an already-constructed error to the diagnostic sink
    /// Used when propagating errors from AST Error variants
    pub fn add_error(&mut self, error: Arc<TranslationError>) {
        self.errors.push(error);
    }

    /// Take ownership of accumulated errors (consumes self)
    /// Converts to TranslationErrors at the boundary
    pub fn take_errors(self) -> TranslationErrors {
        // Convert Vec<Arc<TranslationError>> to TranslationErrors
        // This is the boundary where we unwrap Rc's for the old API
        let unwrapped: Vec<TranslationError> =
            self.errors.into_iter().map(|rc| (*rc).clone()).collect();
        TranslationErrors(unwrapped)
    }
}

impl Default for ParseContext {
    fn default() -> Self {
        Self::new()
    }
}

/// Implement DiagnosticSink for ParseContext
impl DiagnosticSink for ParseContext {
    fn add_error(&mut self, error: Arc<TranslationError>) {
        // Delegate to the public add_error method
        self.add_error(error);
    }
}

/// Trait for converting from CST to AST with error reporting via ParseContext
///
/// This trait is a replacement for `From<T>` that allows passing a mutable
/// `ParseContext` for error accumulation. This enables the diagnostic sink
/// pattern during AST construction from the CST.
pub trait FromCst<T>: Sized {
    /// Convert from CST to AST with error reporting
    fn from_cst_with_context(cst: T, ctx: &mut ParseContext) -> Self;
}

/// Trait for fallible conversion from CST to AST with error reporting
///
/// This trait is used for types that can fail during CST-to-AST conversion
/// (e.g., parsing numeric literals that may be malformed). The error is
/// emitted to the diagnostic sink via the context, and the Result allows
/// the caller to handle the failure (typically by creating an Error variant).
pub trait TryFromCst<T>: Sized {
    /// Try to convert from CST to AST, reporting errors to the context
    ///
    /// If conversion fails, the error is emitted to `ctx` and returned as Err.
    /// The caller can then decide how to handle the failure (e.g., create an
    /// ErrorExpression node).
    fn try_from_cst_with_context(
        cst: T,
        ctx: &mut ParseContext,
    ) -> Result<Self, Arc<TranslationError>>;
}

/// Builder for constructing TranslationErrors with diagnostic sink emission
///
/// This builder accumulates error information (span, source, metadata) and
/// emits the final error to the context's diagnostic sink when `.emit()` is called.
///
/// Generic over `C: DiagnosticSink` to work with both `ParseContext` (parse-phase)
/// and `StatementTranslationContext` (type-check-phase).
pub struct ErrorBuilder<'ctx, C: DiagnosticSink> {
    message: String,
    span: Option<RangeInclusive<usize>>,
    source_desc: Option<String>,
    area: Option<LanguageArea>,
    stage: Stage,
    level: Level,
    supporting: Vec<Context>,
    ctx: &'ctx mut C,
}

impl<'ctx, C: DiagnosticSink> ErrorBuilder<'ctx, C> {
    /// Create a new error builder (internal - use ctx.error() instead)
    pub(crate) fn new(message: String, ctx: &'ctx mut C) -> Self {
        Self {
            message,
            span: None,
            source_desc: None,
            area: None,
            stage: Stage::Translation,
            level: Level::Error,
            supporting: Vec::new(),
            ctx,
        }
    }

    /// Set the source code span for this error
    pub fn at(mut self, spannable: &impl Spannable) -> Self {
        self.span = spannable.span();
        self
    }

    /// Add a source error as context
    pub fn with_source<E>(mut self, source: E) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        self.source_desc = Some(source.to_string());
        self
    }

    /// Add a note string (shown below the source context)
    pub fn with_note(mut self, note: impl Into<String>) -> Self {
        self.source_desc = Some(note.into());
        self
    }

    /// Add a boxed source error as context
    pub fn with_source_boxed(mut self, source: Box<dyn Error + Send + Sync + 'static>) -> Self {
        self.source_desc = Some(source.to_string());
        self
    }

    /// Set the error level (Error, Warning, Info)
    pub fn with_level(mut self, level: Level) -> Self {
        self.level = level;
        self
    }

    /// Set the language area this error occurred in
    pub fn with_area(mut self, area: LanguageArea) -> Self {
        self.area = Some(area);
        self
    }

    /// Set the stage this error occurred in
    pub fn with_stage(mut self, stage: Stage) -> Self {
        self.stage = stage;
        self
    }

    /// Add supporting context with a span and message
    pub fn add_context(mut self, span: RangeInclusive<usize>, message: &str) -> Self {
        self.supporting.push(Context::new(span, message));
        self
    }

    /// Emit the error to the diagnostic sink and return Rc for tree storage
    pub fn emit(self) -> Arc<TranslationError> {
        let error = TranslationError {
            primary: Context::new(self.span.unwrap_or(0..=0), &self.message),
            source_desc: self.source_desc,
            area: self.area,
            stage: self.stage,
            level: self.level,
            supporting: if self.supporting.is_empty() {
                None
            } else {
                Some(self.supporting)
            },
        };

        let rc_error = Arc::new(error);
        self.ctx.add_error(rc_error.clone());
        rc_error
    }
}