runmat 0.5.2

High-performance MATLAB/Octave syntax mathematical runtime
use miette::{SourceOffset, SourceSpan};
use runmat_config::runtime::{self as config, RunMatRuntimeConfig};
use runmat_core::{
    abi::{DiagnosticSeverity, RuntimeDiagnostic},
    RunError,
};
use runmat_runtime::build_runtime_error;

pub fn parser_compat(mode: config::LanguageCompatMode) -> runmat_parser::CompatMode {
    match mode {
        config::LanguageCompatMode::RunMat => runmat_parser::CompatMode::RunMat,
        config::LanguageCompatMode::Matlab => runmat_parser::CompatMode::Matlab,
        config::LanguageCompatMode::Strict => runmat_parser::CompatMode::Strict,
    }
}

pub fn resolved_error_namespace(cfg: &RunMatRuntimeConfig) -> String {
    let configured = cfg.runtime.error_namespace.trim();
    if configured.is_empty() {
        config::error_namespace_for_language_compat(cfg.language.compat).to_string()
    } else {
        configured.to_string()
    }
}

pub fn format_frontend_error(err: &RunError, source_name: &str, source: &str) -> Option<String> {
    match err {
        RunError::Syntax(err) => {
            let mut message = err.message.clone();
            if let Some(expected) = &err.expected {
                message = format!("{message} (expected {expected})");
            }
            if let Some(found) = &err.found_token {
                message = format!("{message} (found '{found}')");
            }
            let span = SourceSpan::new(SourceOffset::from(err.position), 1);
            Some(format_diagnostic(
                &message,
                Some("RunMat:SyntaxError"),
                Some(span),
                source_name,
                source,
            ))
        }
        RunError::Semantic(err) => {
            let span = err.span.map(|span| {
                SourceSpan::new(
                    SourceOffset::from(span.start),
                    span.end.saturating_sub(span.start).max(1),
                )
            });
            let identifier = err.identifier.as_deref().or(Some("RunMat:HirError"));
            Some(format_diagnostic(
                &err.message,
                identifier,
                span,
                source_name,
                source,
            ))
        }
        RunError::Compile(err) => {
            let span = err.span.map(|span| {
                SourceSpan::new(
                    SourceOffset::from(span.start),
                    span.end.saturating_sub(span.start).max(1),
                )
            });
            let identifier = err.identifier.as_deref().or(Some("RunMat:CompileError"));
            Some(format_diagnostic(
                &err.message,
                identifier,
                span,
                source_name,
                source,
            ))
        }
        RunError::Runtime(err) => {
            Some(err.format_diagnostic_with_source(Some(source_name), Some(source)))
        }
    }
}

pub fn format_runtime_diagnostic(
    diagnostic: &RuntimeDiagnostic,
    source_name: Option<&str>,
    source: Option<&str>,
) -> String {
    let span = diagnostic.span.as_ref().map(|span| {
        SourceSpan::new(
            SourceOffset::from(span.start),
            span.end.saturating_sub(span.start).max(1),
        )
    });
    let mut builder = build_runtime_error(diagnostic.message.clone());
    builder = builder.with_identifier(diagnostic.code.clone());
    if let Some(span) = span {
        builder = builder.with_span(span);
    }

    let rendered = builder
        .build()
        .format_diagnostic_with_source(source_name, source);
    let mut rendered = match diagnostic.severity {
        DiagnosticSeverity::Error => rendered,
        severity => {
            let label = diagnostic_severity_label(severity);
            rendered
                .strip_prefix("error: ")
                .map(|rest| format!("{label}: {rest}"))
                .unwrap_or_else(|| format!("{label}: {rendered}"))
        }
    };
    if !diagnostic.callstack.is_empty() {
        rendered.push_str("\ncallstack:");
        if diagnostic.callstack_elided > 0 {
            rendered.push_str(&format!(
                "\n  ... {} frames elided ...",
                diagnostic.callstack_elided
            ));
        }
        for frame in &diagnostic.callstack {
            rendered.push_str(&format!("\n  {frame}"));
        }
    }
    rendered
}

fn diagnostic_severity_label(severity: DiagnosticSeverity) -> &'static str {
    match severity {
        DiagnosticSeverity::Error => "error",
        DiagnosticSeverity::Warning => "warning",
        DiagnosticSeverity::Info => "info",
        DiagnosticSeverity::Hint => "hint",
    }
}

pub fn format_diagnostic(
    message: &str,
    identifier: Option<&str>,
    span: Option<SourceSpan>,
    source_name: &str,
    source: &str,
) -> String {
    let mut builder = build_runtime_error(message);
    if let Some(identifier) = identifier {
        builder = builder.with_identifier(identifier);
    }
    if let Some(span) = span {
        builder = builder.with_span(span);
    }
    builder
        .build()
        .format_diagnostic_with_source(Some(source_name), Some(source))
}

#[cfg(test)]
mod compat_tests {
    use super::*;
    use runmat_core::abi::{DiagnosticSeverity, RuntimeDiagnostic};

    #[test]
    fn resolved_error_namespace_defaults_from_language_compat() {
        let mut cfg = RunMatRuntimeConfig::default();
        cfg.runtime.error_namespace.clear();

        cfg.language.compat = config::LanguageCompatMode::RunMat;
        assert_eq!(resolved_error_namespace(&cfg), "RunMat");

        cfg.language.compat = config::LanguageCompatMode::Matlab;
        assert_eq!(resolved_error_namespace(&cfg), "MATLAB");

        cfg.language.compat = config::LanguageCompatMode::Strict;
        assert_eq!(resolved_error_namespace(&cfg), "RunMat");
    }

    #[test]
    fn resolved_error_namespace_honors_explicit_override() {
        let mut cfg = RunMatRuntimeConfig::default();
        cfg.language.compat = config::LanguageCompatMode::Matlab;
        cfg.runtime.error_namespace = "CustomNS".to_string();
        assert_eq!(resolved_error_namespace(&cfg), "CustomNS");
    }

    #[test]
    fn runtime_diagnostic_render_includes_source_and_callstack() {
        let diagnostic = RuntimeDiagnostic {
            code: "RunMat:UndefinedFunction".to_string(),
            severity: DiagnosticSeverity::Error,
            message: "Undefined function: butter".to_string(),
            span: Some(runmat_hir::Span { start: 4, end: 10 }),
            callstack: vec!["main".to_string()],
            callstack_elided: 0,
        };

        let rendered =
            format_runtime_diagnostic(&diagnostic, Some("main.m"), Some("y = butter(4);"));

        assert!(rendered.contains("error: Undefined function: butter"));
        assert!(rendered.contains("id: RunMat:UndefinedFunction"));
        assert!(rendered.contains("--> main.m:1:5"));
        assert!(rendered.contains("callstack:\n  main"));
    }

    #[test]
    fn runtime_diagnostic_render_preserves_warning_severity() {
        let diagnostic = RuntimeDiagnostic {
            code: "RunMat:Warning".to_string(),
            severity: DiagnosticSeverity::Warning,
            message: "careful".to_string(),
            span: None,
            callstack: Vec::new(),
            callstack_elided: 0,
        };

        let rendered = format_runtime_diagnostic(&diagnostic, None, None);

        assert!(rendered.starts_with("warning: careful"));
        assert!(!rendered.starts_with("error: careful"));
    }
}