sigil_parser/
diagnostic.rs

1//! Rich diagnostic reporting for Sigil.
2//!
3//! Provides Rust-quality error messages with:
4//! - Colored output with source context
5//! - "Did you mean?" suggestions
6//! - Fix suggestions
7//! - Multi-span support for related information
8//! - JSON output for AI agent consumption
9
10use ariadne::{Color, ColorGenerator, Config, Fmt, Label, Report, ReportKind, Source};
11use serde::{Deserialize, Serialize};
12use std::ops::Range;
13use strsim::jaro_winkler;
14
15use crate::lexer::Token;
16use crate::span::Span;
17
18/// Severity level for diagnostics.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum Severity {
22    Error,
23    Warning,
24    Info,
25    Hint,
26}
27
28impl Severity {
29    fn to_report_kind(self) -> ReportKind<'static> {
30        match self {
31            Severity::Error => ReportKind::Error,
32            Severity::Warning => ReportKind::Warning,
33            Severity::Info => ReportKind::Advice,
34            Severity::Hint => ReportKind::Advice,
35        }
36    }
37
38    fn color(self) -> Color {
39        match self {
40            Severity::Error => Color::Red,
41            Severity::Warning => Color::Yellow,
42            Severity::Info => Color::Blue,
43            Severity::Hint => Color::Cyan,
44        }
45    }
46}
47
48/// A fix suggestion that can be applied automatically.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct FixSuggestion {
51    pub message: String,
52    pub span: Span,
53    pub replacement: String,
54}
55
56/// A related location with additional context.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct RelatedInfo {
59    pub message: String,
60    pub span: Span,
61}
62
63/// A label at a specific location.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct DiagnosticLabel {
66    pub span: Span,
67    pub message: String,
68}
69
70/// A rich diagnostic with all context needed for beautiful error reporting.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Diagnostic {
73    pub severity: Severity,
74    pub code: Option<String>,
75    pub message: String,
76    pub span: Span,
77    #[serde(skip)]
78    pub labels: Vec<(Span, String)>,
79    pub notes: Vec<String>,
80    pub suggestions: Vec<FixSuggestion>,
81    pub related: Vec<RelatedInfo>,
82}
83
84/// JSON-serializable diagnostic output for AI agents.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct JsonDiagnostic {
87    pub severity: Severity,
88    pub code: Option<String>,
89    pub message: String,
90    pub file: String,
91    pub span: Span,
92    pub line: u32,
93    pub column: u32,
94    pub end_line: u32,
95    pub end_column: u32,
96    pub labels: Vec<DiagnosticLabel>,
97    pub notes: Vec<String>,
98    pub suggestions: Vec<FixSuggestion>,
99    pub related: Vec<RelatedInfo>,
100}
101
102/// JSON output wrapper for multiple diagnostics.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct JsonDiagnosticsOutput {
105    pub file: String,
106    pub diagnostics: Vec<JsonDiagnostic>,
107    pub error_count: usize,
108    pub warning_count: usize,
109    pub success: bool,
110}
111
112impl Diagnostic {
113    /// Create a new error diagnostic.
114    pub fn error(message: impl Into<String>, span: Span) -> Self {
115        Self {
116            severity: Severity::Error,
117            code: None,
118            message: message.into(),
119            span,
120            labels: Vec::new(),
121            notes: Vec::new(),
122            suggestions: Vec::new(),
123            related: Vec::new(),
124        }
125    }
126
127    /// Create a new warning diagnostic.
128    pub fn warning(message: impl Into<String>, span: Span) -> Self {
129        Self {
130            severity: Severity::Warning,
131            code: None,
132            message: message.into(),
133            span,
134            labels: Vec::new(),
135            notes: Vec::new(),
136            suggestions: Vec::new(),
137            related: Vec::new(),
138        }
139    }
140
141    /// Add an error code (e.g., "E0001").
142    pub fn with_code(mut self, code: impl Into<String>) -> Self {
143        self.code = Some(code.into());
144        self
145    }
146
147    /// Add a label at a specific span.
148    pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
149        self.labels.push((span, message.into()));
150        self
151    }
152
153    /// Add a note (shown at the bottom).
154    pub fn with_note(mut self, note: impl Into<String>) -> Self {
155        self.notes.push(note.into());
156        self
157    }
158
159    /// Add a fix suggestion.
160    pub fn with_suggestion(
161        mut self,
162        message: impl Into<String>,
163        span: Span,
164        replacement: impl Into<String>,
165    ) -> Self {
166        self.suggestions.push(FixSuggestion {
167            message: message.into(),
168            span,
169            replacement: replacement.into(),
170        });
171        self
172    }
173
174    /// Add related information.
175    pub fn with_related(mut self, message: impl Into<String>, span: Span) -> Self {
176        self.related.push(RelatedInfo {
177            message: message.into(),
178            span,
179        });
180        self
181    }
182
183    /// Render this diagnostic to a string with colors.
184    pub fn render(&self, filename: &str, source: &str) -> String {
185        let mut output = Vec::new();
186        self.write_to(&mut output, filename, source);
187        String::from_utf8(output).unwrap_or_else(|_| self.message.clone())
188    }
189
190    /// Write this diagnostic to a writer.
191    pub fn write_to<W: std::io::Write>(&self, writer: W, filename: &str, source: &str) {
192        let span_range: Range<usize> = self.span.start..self.span.end;
193
194        let mut colors = ColorGenerator::new();
195        let primary_color = self.severity.color();
196
197        let mut builder = Report::build(self.severity.to_report_kind(), filename, self.span.start)
198            .with_config(Config::default().with_cross_gap(true))
199            .with_message(&self.message);
200
201        // Add error code if present
202        if let Some(ref code) = self.code {
203            builder = builder.with_code(code);
204        }
205
206        // Primary label
207        builder = builder.with_label(
208            Label::new((filename, span_range.clone()))
209                .with_message(&self.message)
210                .with_color(primary_color),
211        );
212
213        // Additional labels
214        for (span, msg) in &self.labels {
215            let color = colors.next();
216            builder = builder.with_label(
217                Label::new((filename, span.start..span.end))
218                    .with_message(msg)
219                    .with_color(color),
220            );
221        }
222
223        // Notes
224        for note in &self.notes {
225            builder = builder.with_note(note);
226        }
227
228        // Suggestions as notes
229        for suggestion in &self.suggestions {
230            let help_msg = format!(
231                "help: {}: `{}`",
232                suggestion.message,
233                suggestion.replacement.clone().fg(Color::Green)
234            );
235            builder = builder.with_help(help_msg);
236        }
237
238        builder
239            .finish()
240            .write((filename, Source::from(source)), writer)
241            .unwrap();
242    }
243
244    /// Print this diagnostic to stderr.
245    pub fn eprint(&self, filename: &str, source: &str) {
246        self.write_to(std::io::stderr(), filename, source);
247    }
248
249    /// Convert to JSON-serializable format with computed line/column positions.
250    pub fn to_json(&self, filename: &str, source: &str) -> JsonDiagnostic {
251        let (line, column) = offset_to_line_col(source, self.span.start);
252        let (end_line, end_column) = offset_to_line_col(source, self.span.end);
253
254        JsonDiagnostic {
255            severity: self.severity,
256            code: self.code.clone(),
257            message: self.message.clone(),
258            file: filename.to_string(),
259            span: self.span,
260            line,
261            column,
262            end_line,
263            end_column,
264            labels: self
265                .labels
266                .iter()
267                .map(|(span, msg)| DiagnosticLabel {
268                    span: *span,
269                    message: msg.clone(),
270                })
271                .collect(),
272            notes: self.notes.clone(),
273            suggestions: self.suggestions.clone(),
274            related: self.related.clone(),
275        }
276    }
277}
278
279/// Convert byte offset to line/column (1-indexed).
280fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
281    let mut line = 1u32;
282    let mut col = 1u32;
283
284    for (i, ch) in source.char_indices() {
285        if i >= offset {
286            break;
287        }
288        if ch == '\n' {
289            line += 1;
290            col = 1;
291        } else {
292            col += 1;
293        }
294    }
295
296    (line, col)
297}
298
299/// Diagnostic builder for common error patterns.
300pub struct DiagnosticBuilder;
301
302impl DiagnosticBuilder {
303    /// Create an "unexpected token" error with suggestions.
304    pub fn unexpected_token(expected: &str, found: &Token, span: Span, source: &str) -> Diagnostic {
305        let found_str = format!("{:?}", found);
306        let message = format!("expected {}, found {}", expected, found_str);
307
308        let mut diag = Diagnostic::error(message, span).with_code("E0001");
309
310        // Add context about what was expected
311        diag = diag.with_label(span, format!("expected {} here", expected));
312
313        // Add suggestions for common mistakes
314        if let Some(suggestion) = Self::suggest_token_fix(expected, found, source, span) {
315            diag = diag.with_suggestion(suggestion.0, span, suggestion.1);
316        }
317
318        diag
319    }
320
321    /// Create an "undefined variable" error with "did you mean?" suggestions.
322    pub fn undefined_variable(name: &str, span: Span, known_names: &[&str]) -> Diagnostic {
323        let message = format!("cannot find value `{}` in this scope", name);
324        let mut diag = Diagnostic::error(message, span)
325            .with_code("E0425")
326            .with_label(span, "not found in this scope");
327
328        // Find similar names
329        if let Some(suggestion) = Self::find_similar(name, known_names) {
330            diag = diag.with_suggestion(
331                format!("a local variable with a similar name exists"),
332                span,
333                suggestion.to_string(),
334            );
335        }
336
337        diag
338    }
339
340    /// Create a "type mismatch" error.
341    pub fn type_mismatch(
342        expected: &str,
343        found: &str,
344        span: Span,
345        expected_span: Option<Span>,
346    ) -> Diagnostic {
347        let message = format!(
348            "mismatched types: expected `{}`, found `{}`",
349            expected, found
350        );
351        let mut diag = Diagnostic::error(message, span)
352            .with_code("E0308")
353            .with_label(span, format!("expected `{}`", expected));
354
355        if let Some(exp_span) = expected_span {
356            diag = diag.with_related("expected due to this", exp_span);
357        }
358
359        diag
360    }
361
362    /// Create an "evidentiality mismatch" error.
363    pub fn evidentiality_mismatch(expected: &str, found: &str, span: Span) -> Diagnostic {
364        let message = format!(
365            "evidentiality mismatch: expected `{}`, found `{}`",
366            expected, found
367        );
368
369        Diagnostic::error(message, span)
370            .with_code("E0600")
371            .with_label(span, format!("has evidentiality `{}`", found))
372            .with_note(format!(
373                "values with `{}` evidentiality cannot be used where `{}` is required",
374                found, expected
375            ))
376            .with_note(Self::evidentiality_help(expected, found))
377    }
378
379    /// Create an "untrusted data" error for evidentiality violations.
380    pub fn untrusted_data_used(span: Span, source_span: Option<Span>) -> Diagnostic {
381        let mut diag = Diagnostic::error("cannot use reported (~) data without validation", span)
382            .with_code("E0601")
383            .with_label(span, "untrusted data used here")
384            .with_note("data from external sources must be validated before use")
385            .with_suggestion("validate the data first", span, "value|validate!{...}");
386
387        if let Some(src) = source_span {
388            diag = diag.with_related("data originates from external source here", src);
389        }
390
391        diag
392    }
393
394    /// Create a "missing morpheme" error with ASCII alternatives.
395    pub fn unknown_morpheme(found: &str, span: Span) -> Diagnostic {
396        let message = format!("unknown morpheme `{}`", found);
397        let mut diag = Diagnostic::error(message, span).with_code("E0100");
398
399        // Suggest similar morphemes
400        let morphemes = [
401            ("τ", "tau", "transform/map"),
402            ("φ", "phi", "filter"),
403            ("σ", "sigma", "sort"),
404            ("ρ", "rho", "reduce"),
405            ("λ", "lambda", "anonymous function"),
406            ("Σ", "sum", "sum all"),
407            ("Π", "pi", "product"),
408            ("α", "alpha", "first element"),
409            ("ω", "omega", "last element"),
410            ("μ", "mu", "middle element"),
411            ("χ", "chi", "random choice"),
412            ("ν", "nu", "nth element"),
413            ("ξ", "xi", "next in sequence"),
414        ];
415
416        if let Some((greek, _ascii, desc)) = morphemes
417            .iter()
418            .find(|(g, a, _)| jaro_winkler(found, g) > 0.8 || jaro_winkler(found, a) > 0.8)
419        {
420            diag = diag.with_suggestion(
421                format!("did you mean the {} morpheme?", desc),
422                span,
423                greek.to_string(),
424            );
425        }
426
427        diag = diag.with_note(
428            "transform morphemes: τ (map), φ (filter), σ (sort), ρ (reduce), Σ (sum), Π (product)",
429        );
430        diag = diag.with_note(
431            "access morphemes: α (first), ω (last), μ (middle), χ (choice), ν (nth), ξ (next)",
432        );
433
434        diag
435    }
436
437    /// Suggest a Unicode symbol for an ASCII operator or name.
438    /// Returns (unicode_symbol, description) if a suggestion exists.
439    pub fn suggest_unicode_symbol(ascii: &str) -> Option<(&'static str, &'static str)> {
440        match ascii {
441            // Logic operators
442            "&&" => Some(("∧", "logical AND")),
443            "||" => Some(("∨", "logical OR")),
444            "^^" => Some(("⊻", "logical XOR")),
445
446            // Bitwise operators
447            "&" => Some(("⋏", "bitwise AND")),
448            "|" => Some(("⋎", "bitwise OR")),
449
450            // Set operations
451            "union" => Some(("∪", "set union")),
452            "intersect" | "intersection" => Some(("∩", "set intersection")),
453            "subset" => Some(("⊂", "proper subset")),
454            "superset" => Some(("⊃", "proper superset")),
455            "in" | "element_of" => Some(("∈", "element of")),
456            "not_in" => Some(("∉", "not element of")),
457
458            // Math symbols
459            "sqrt" => Some(("√", "square root")),
460            "cbrt" => Some(("∛", "cube root")),
461            "infinity" | "inf" => Some(("∞", "infinity")),
462            "pi" => Some(("π", "pi constant")),
463            "sum" => Some(("Σ", "summation")),
464            "product" => Some(("Π", "product")),
465            "integral" => Some(("∫", "integral/cumulative sum")),
466            "partial" | "derivative" => Some(("∂", "partial/derivative")),
467
468            // Morphemes
469            "tau" | "map" | "transform" => Some(("τ", "transform morpheme")),
470            "phi" | "filter" => Some(("φ", "filter morpheme")),
471            "sigma" | "sort" => Some(("σ", "sort morpheme")),
472            "rho" | "reduce" | "fold" => Some(("ρ", "reduce morpheme")),
473            "lambda" => Some(("λ", "lambda")),
474            "alpha" | "first" => Some(("α", "first element")),
475            "omega" | "last" => Some(("ω", "last element")),
476            "mu" | "middle" | "median" => Some(("μ", "middle element")),
477            "chi" | "choice" | "random" => Some(("χ", "random choice")),
478            "nu" | "nth" => Some(("ν", "nth element")),
479            "xi" | "next" => Some(("ξ", "next in sequence")),
480            "delta" | "diff" | "change" => Some(("δ", "delta/change")),
481            "epsilon" | "empty" => Some(("ε", "epsilon/empty")),
482            "zeta" | "zip" => Some(("ζ", "zeta/zip")),
483
484            // Category theory
485            "compose" => Some(("∘", "function composition")),
486            "tensor" => Some(("⊗", "tensor product")),
487            "direct_sum" | "xor" => Some(("⊕", "direct sum/XOR")),
488
489            // Special values
490            "null" | "void" | "nothing" => Some(("∅", "empty set")),
491            "true" | "top" | "any" => Some(("⊤", "top/true")),
492            "false" | "bottom" | "never" => Some(("⊥", "bottom/false")),
493
494            // Quantifiers
495            "forall" | "for_all" => Some(("∀", "universal quantifier")),
496            "exists" => Some(("∃", "existential quantifier")),
497
498            // Data operations
499            "join" | "zip_with" => Some(("⋈", "join/zip with")),
500            "flatten" => Some(("⋳", "flatten")),
501            "max" | "supremum" => Some(("⊔", "supremum/max")),
502            "min" | "infimum" => Some(("⊓", "infimum/min")),
503
504            _ => None,
505        }
506    }
507
508    /// Create a hint diagnostic suggesting a Unicode symbol.
509    pub fn suggest_symbol_upgrade(ascii: &str, span: Span) -> Option<Diagnostic> {
510        Self::suggest_unicode_symbol(ascii).map(|(unicode, desc)| {
511            Diagnostic::warning(
512                format!("consider using Unicode symbol `{}` for {}", unicode, desc),
513                span,
514            )
515            .with_code("W0200")
516            .with_suggestion(
517                format!("use `{}` for clearer, more idiomatic Sigil", unicode),
518                span,
519                unicode.to_string(),
520            )
521            .with_note(format!(
522                "Sigil supports Unicode symbols. `{}` → `{}` ({})",
523                ascii, unicode, desc
524            ))
525        })
526    }
527
528    /// Get all available symbol mappings for documentation/completion.
529    pub fn all_symbol_mappings() -> Vec<(&'static str, &'static str, &'static str)> {
530        vec![
531            // (ascii, unicode, description)
532            ("&&", "∧", "logical AND"),
533            ("||", "∨", "logical OR"),
534            ("^^", "⊻", "logical XOR"),
535            ("&", "⋏", "bitwise AND"),
536            ("|", "⋎", "bitwise OR"),
537            ("union", "∪", "set union"),
538            ("intersect", "∩", "set intersection"),
539            ("subset", "⊂", "proper subset"),
540            ("superset", "⊃", "proper superset"),
541            ("in", "∈", "element of"),
542            ("not_in", "∉", "not element of"),
543            ("sqrt", "√", "square root"),
544            ("cbrt", "∛", "cube root"),
545            ("infinity", "∞", "infinity"),
546            ("tau", "τ", "transform"),
547            ("phi", "φ", "filter"),
548            ("sigma", "σ", "sort"),
549            ("rho", "ρ", "reduce"),
550            ("lambda", "λ", "lambda"),
551            ("alpha", "α", "first"),
552            ("omega", "ω", "last"),
553            ("mu", "μ", "middle"),
554            ("chi", "χ", "choice"),
555            ("nu", "ν", "nth"),
556            ("xi", "ξ", "next"),
557            ("sum", "Σ", "sum"),
558            ("product", "Π", "product"),
559            ("compose", "∘", "compose"),
560            ("tensor", "⊗", "tensor"),
561            ("xor", "⊕", "direct sum"),
562            ("forall", "∀", "for all"),
563            ("exists", "∃", "exists"),
564            ("null", "∅", "empty"),
565            ("true", "⊤", "top/true"),
566            ("false", "⊥", "bottom/false"),
567        ]
568    }
569
570    /// Find a similar name using Jaro-Winkler distance.
571    fn find_similar<'a>(name: &str, candidates: &[&'a str]) -> Option<&'a str> {
572        candidates
573            .iter()
574            .filter(|c| jaro_winkler(name, c) > 0.8)
575            .max_by(|a, b| {
576                jaro_winkler(name, a)
577                    .partial_cmp(&jaro_winkler(name, b))
578                    .unwrap_or(std::cmp::Ordering::Equal)
579            })
580            .copied()
581    }
582
583    /// Suggest fixes for common token mistakes.
584    fn suggest_token_fix(
585        expected: &str,
586        found: &Token,
587        _source: &str,
588        _span: Span,
589    ) -> Option<(String, String)> {
590        // Common mistake patterns
591        match (expected, found) {
592            ("`;`", Token::RBrace) => Some((
593                "you might be missing a semicolon".to_string(),
594                ";".to_string(),
595            )),
596            ("`{`", Token::Arrow) => Some((
597                "you might want a block here".to_string(),
598                "{ ... }".to_string(),
599            )),
600            ("`)`", Token::Comma) => Some((
601                "unexpected comma, maybe close the parenthesis first".to_string(),
602                ")".to_string(),
603            )),
604            _ => None,
605        }
606    }
607
608    /// Generate help text for evidentiality issues.
609    fn evidentiality_help(expected: &str, found: &str) -> String {
610        match (expected, found) {
611            ("!", "~") => {
612                "use `value|validate!{...}` to promote reported data to known".to_string()
613            }
614            ("!", "?") => {
615                "handle the uncertain case with `match` or unwrap with `value!`".to_string()
616            }
617            ("?", "~") => "reported data is already uncertain, no conversion needed".to_string(),
618            _ => format!(
619                "evidentiality flows: ! (known) < ? (uncertain) < ~ (reported) < ‽ (paradox)"
620            ),
621        }
622    }
623}
624
625/// Collection of diagnostics for a compilation unit.
626#[derive(Debug, Default)]
627pub struct Diagnostics {
628    items: Vec<Diagnostic>,
629    has_errors: bool,
630}
631
632impl Diagnostics {
633    pub fn new() -> Self {
634        Self::default()
635    }
636
637    pub fn add(&mut self, diagnostic: Diagnostic) {
638        if diagnostic.severity == Severity::Error {
639            self.has_errors = true;
640        }
641        self.items.push(diagnostic);
642    }
643
644    pub fn has_errors(&self) -> bool {
645        self.has_errors
646    }
647
648    pub fn is_empty(&self) -> bool {
649        self.items.is_empty()
650    }
651
652    pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
653        self.items.iter()
654    }
655
656    /// Render all diagnostics.
657    pub fn render_all(&self, filename: &str, source: &str) -> String {
658        let mut output = String::new();
659        for diag in &self.items {
660            output.push_str(&diag.render(filename, source));
661            output.push('\n');
662        }
663        output
664    }
665
666    /// Print all diagnostics to stderr.
667    pub fn eprint_all(&self, filename: &str, source: &str) {
668        for diag in &self.items {
669            diag.eprint(filename, source);
670        }
671    }
672
673    /// Get error count.
674    pub fn error_count(&self) -> usize {
675        self.items
676            .iter()
677            .filter(|d| d.severity == Severity::Error)
678            .count()
679    }
680
681    /// Get warning count.
682    pub fn warning_count(&self) -> usize {
683        self.items
684            .iter()
685            .filter(|d| d.severity == Severity::Warning)
686            .count()
687    }
688
689    /// Print summary.
690    pub fn print_summary(&self) {
691        let errors = self.error_count();
692        let warnings = self.warning_count();
693
694        if errors > 0 || warnings > 0 {
695            eprint!("\n");
696            if errors > 0 {
697                eprintln!(
698                    "{}: aborting due to {} previous error{}",
699                    "error".fg(Color::Red),
700                    errors,
701                    if errors == 1 { "" } else { "s" }
702                );
703            }
704            if warnings > 0 {
705                eprintln!(
706                    "{}: {} warning{} emitted",
707                    "warning".fg(Color::Yellow),
708                    warnings,
709                    if warnings == 1 { "" } else { "s" }
710                );
711            }
712        }
713    }
714
715    /// Convert all diagnostics to JSON output format.
716    ///
717    /// Returns a structured JSON object suitable for machine consumption,
718    /// with line/column positions, fix suggestions, and metadata.
719    pub fn to_json_output(&self, filename: &str, source: &str) -> JsonDiagnosticsOutput {
720        let diagnostics: Vec<JsonDiagnostic> = self
721            .items
722            .iter()
723            .map(|d| d.to_json(filename, source))
724            .collect();
725
726        let error_count = self.error_count();
727        let warning_count = self.warning_count();
728
729        JsonDiagnosticsOutput {
730            file: filename.to_string(),
731            diagnostics,
732            error_count,
733            warning_count,
734            success: error_count == 0,
735        }
736    }
737
738    /// Render diagnostics as JSON string.
739    ///
740    /// For AI agent consumption - provides structured, machine-readable output.
741    pub fn to_json_string(&self, filename: &str, source: &str) -> String {
742        let output = self.to_json_output(filename, source);
743        serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
744    }
745
746    /// Render diagnostics as compact JSON (single line).
747    ///
748    /// For piping to other tools or streaming output.
749    pub fn to_json_compact(&self, filename: &str, source: &str) -> String {
750        let output = self.to_json_output(filename, source);
751        serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string())
752    }
753}
754
755// ============================================================================
756// Error Type Conversions
757// ============================================================================
758
759use crate::parser::ParseError;
760use crate::typeck::{TypeError, TypeErrorCode};
761
762impl From<ParseError> for Diagnostic {
763    fn from(err: ParseError) -> Self {
764        match err {
765            ParseError::UnexpectedToken {
766                expected,
767                found,
768                span,
769            } => {
770                let found_str = format_token(&found);
771                let message = format!("expected {}, found {}", expected, found_str);
772
773                let mut diag = Diagnostic::error(message, span)
774                    .with_code("E0001")
775                    .with_label(span, format!("expected {} here", expected));
776
777                // Add contextual suggestions based on common mistakes
778                diag = add_token_suggestions(diag, &expected, &found, span);
779                diag
780            }
781            ParseError::UnexpectedEof => {
782                Diagnostic::error("unexpected end of file", Span::new(0, 0))
783                    .with_code("E0002")
784                    .with_note("the file ended unexpectedly while parsing")
785                    .with_note("check for unclosed braces, brackets, or parentheses")
786            }
787            ParseError::InvalidNumber(msg) => {
788                Diagnostic::error(format!("invalid number literal: {}", msg), Span::new(0, 0))
789                    .with_code("E0003")
790                    .with_note("number literals must be valid integers or floats")
791            }
792            ParseError::Custom(msg) => {
793                Diagnostic::error(msg, Span::new(0, 0)).with_code("E0004")
794            }
795        }
796    }
797}
798
799impl From<&TypeError> for Diagnostic {
800    fn from(err: &TypeError) -> Self {
801        let span = err.span.unwrap_or_default();
802        let mut diag = Diagnostic::error(&err.message, span).with_code(err.code.code());
803
804        // Add contextual help based on error type
805        match err.code {
806            TypeErrorCode::TypeMismatch => {
807                diag = diag.with_note("types must match for this operation");
808            }
809            TypeErrorCode::UndefinedName => {
810                diag = diag.with_note("check spelling or add an import/definition");
811            }
812            TypeErrorCode::BorrowError => {
813                diag = diag.with_note("a value can only be borrowed once mutably, or multiple times immutably");
814            }
815            TypeErrorCode::EvidentialityError => {
816                diag = diag.with_note("evidentiality markers track the source and certainty of values");
817            }
818            TypeErrorCode::NonBoolCondition => {
819                diag = diag.with_note("conditions must evaluate to `true` or `false`");
820            }
821            TypeErrorCode::HeterogeneousArray => {
822                diag = diag.with_note("all elements in an array must have the same type");
823            }
824            TypeErrorCode::InvalidIndex => {
825                diag = diag.with_note("use integers like `0`, `1`, `2` to index arrays");
826            }
827            TypeErrorCode::InvalidOperand => {
828                diag = diag.with_note("check that the operand types are valid for this operator");
829            }
830            TypeErrorCode::MissingMatchArm => {
831                diag = diag.with_note("add at least one arm to handle the matched value");
832            }
833            TypeErrorCode::InvalidReduction => {
834                diag = diag.with_note("reduction operations work on arrays or slices");
835            }
836            TypeErrorCode::Generic => {}
837        }
838
839        // Add notes from the error
840        for note in &err.notes {
841            diag = diag.with_note(note);
842        }
843
844        diag
845    }
846}
847
848impl From<TypeError> for Diagnostic {
849    fn from(err: TypeError) -> Self {
850        Diagnostic::from(&err)
851    }
852}
853
854use crate::interpreter::{RuntimeError, RuntimeErrorCode};
855
856impl From<&RuntimeError> for Diagnostic {
857    fn from(err: &RuntimeError) -> Self {
858        let span = err.span.unwrap_or_default();
859        let mut diag = Diagnostic::error(&err.message, span).with_code(err.code.code());
860
861        // Add contextual help based on error type
862        match err.code {
863            RuntimeErrorCode::DivisionByZero => {
864                diag = diag.with_note("check that the divisor is not zero before dividing");
865            }
866            RuntimeErrorCode::IndexOutOfBounds => {
867                diag = diag.with_note("array indices must be between 0 and length - 1");
868            }
869            RuntimeErrorCode::UndefinedVariable => {
870                diag = diag.with_note("make sure the variable is defined before use");
871            }
872            RuntimeErrorCode::TypeError => {
873                diag = diag.with_note("values must have compatible types at runtime");
874            }
875            RuntimeErrorCode::InvalidOperation => {
876                diag = diag.with_note("this operation is not supported for this value");
877            }
878            RuntimeErrorCode::AssertionFailed => {
879                diag = diag.with_note("the assertion condition evaluated to false");
880            }
881            RuntimeErrorCode::Overflow => {
882                diag = diag.with_note("the result is too large to represent");
883            }
884            RuntimeErrorCode::StackOverflow => {
885                diag = diag.with_note("check for infinite recursion in your code");
886            }
887            RuntimeErrorCode::ControlFlowError => {
888                diag = diag.with_note("control flow statements must be used in the correct context");
889            }
890            RuntimeErrorCode::LinearTypeViolation => {
891                diag = diag.with_note("linear values can only be used once (no-cloning theorem)");
892            }
893            RuntimeErrorCode::Generic => {}
894        }
895
896        diag
897    }
898}
899
900impl From<RuntimeError> for Diagnostic {
901    fn from(err: RuntimeError) -> Self {
902        Diagnostic::from(&err)
903    }
904}
905
906/// Format a token for display in error messages.
907fn format_token(token: &Token) -> String {
908    match token {
909        Token::Ident(s) => format!("identifier `{}`", s),
910        Token::IntLit(n) => format!("integer `{}`", n),
911        Token::FloatLit(f) => format!("float `{}`", f),
912        Token::StringLit(s) => format!("string {:?}", s),
913        Token::CharLit(c) => format!("character {:?}", c),
914        Token::LParen => "`(`".to_string(),
915        Token::RParen => "`)`".to_string(),
916        Token::LBrace => "`{`".to_string(),
917        Token::RBrace => "`}`".to_string(),
918        Token::LBracket => "`[`".to_string(),
919        Token::RBracket => "`]`".to_string(),
920        Token::Semi => "`;`".to_string(),
921        Token::Colon => "`:`".to_string(),
922        Token::ColonColon => "`::`".to_string(),
923        Token::Comma => "`,`".to_string(),
924        Token::Dot => "`.`".to_string(),
925        Token::DotDot => "`..`".to_string(),
926        Token::Arrow => "`->`".to_string(),
927        Token::FatArrow => "`=>`".to_string(),
928        Token::Eq => "`=`".to_string(),
929        Token::EqEq => "`==`".to_string(),
930        Token::NotEq => "`!=`".to_string(),
931        Token::Lt => "`<`".to_string(),
932        Token::LtEq => "`<=`".to_string(),
933        Token::Gt => "`>`".to_string(),
934        Token::GtEq => "`>=`".to_string(),
935        Token::Plus => "`+`".to_string(),
936        Token::Minus => "`-`".to_string(),
937        Token::Star => "`*`".to_string(),
938        Token::Slash => "`/`".to_string(),
939        Token::Percent => "`%`".to_string(),
940        Token::Amp => "`&`".to_string(),
941        Token::Pipe => "`|`".to_string(),
942        Token::AndAnd => "`&&`".to_string(),
943        Token::OrOr => "`||`".to_string(),
944        Token::Bang => "`!`".to_string(),
945        Token::Question => "`?`".to_string(),
946        Token::Tilde => "`~`".to_string(),
947        Token::Caret => "`^`".to_string(),
948        Token::Fn => "`fn`".to_string(),
949        Token::Let => "`let`".to_string(),
950        Token::Mut => "`mut`".to_string(),
951        Token::If => "`if`".to_string(),
952        Token::Else => "`else`".to_string(),
953        Token::While => "`while`".to_string(),
954        Token::For => "`for`".to_string(),
955        Token::In => "`in`".to_string(),
956        Token::Return => "`return`".to_string(),
957        Token::Break => "`break`".to_string(),
958        Token::Continue => "`continue`".to_string(),
959        Token::Struct => "`struct`".to_string(),
960        Token::Enum => "`enum`".to_string(),
961        Token::Impl => "`impl`".to_string(),
962        Token::Trait => "`trait`".to_string(),
963        Token::Pub => "`pub`".to_string(),
964        Token::Use => "`use`".to_string(),
965        Token::Mod => "`mod`".to_string(),
966        Token::True => "`true`".to_string(),
967        Token::False => "`false`".to_string(),
968        Token::Match => "`match`".to_string(),
969        Token::SelfLower => "`self`".to_string(),
970        Token::SelfUpper => "`Self`".to_string(),
971        Token::Const => "`const`".to_string(),
972        Token::Static => "`static`".to_string(),
973        Token::Type => "`type`".to_string(),
974        Token::Where => "`where`".to_string(),
975        Token::As => "`as`".to_string(),
976        Token::Async => "`async`".to_string(),
977        Token::Await => "`await`".to_string(),
978        Token::Unsafe => "`unsafe`".to_string(),
979        Token::Extern => "`extern`".to_string(),
980        Token::Crate => "`crate`".to_string(),
981        Token::Super => "`super`".to_string(),
982        Token::Dyn => "`dyn`".to_string(),
983        Token::Move => "`move`".to_string(),
984        Token::Ref => "`ref`".to_string(),
985        Token::Loop => "`loop`".to_string(),
986        _ => format!("`{:?}`", token),
987    }
988}
989
990/// Add contextual suggestions for common token mistakes.
991fn add_token_suggestions(mut diag: Diagnostic, expected: &str, found: &Token, span: Span) -> Diagnostic {
992    // Common mistake: missing semicolon
993    if expected.contains(';') && matches!(found, Token::RBrace | Token::Fn | Token::Let | Token::Struct) {
994        diag = diag
995            .with_note("statements must end with a semicolon")
996            .with_suggestion("add a semicolon", span, ";");
997    }
998
999    // Common mistake: using = instead of ==
1000    if expected.contains("==") && matches!(found, Token::Eq) {
1001        diag = diag
1002            .with_note("use `==` for comparison, `=` is for assignment")
1003            .with_suggestion("use comparison operator", span, "==");
1004    }
1005
1006    // Common mistake: wrong brace type
1007    if expected.contains('{') && matches!(found, Token::LParen) {
1008        diag = diag
1009            .with_note("blocks use curly braces `{}`")
1010            .with_suggestion("use curly brace", span, "{");
1011    }
1012
1013    // Common mistake: missing closing delimiter
1014    if expected.contains(')') && matches!(found, Token::Semi | Token::RBrace) {
1015        diag = diag.with_note("you may have unclosed parentheses");
1016    }
1017    if expected.contains(']') && matches!(found, Token::Semi | Token::RBrace) {
1018        diag = diag.with_note("you may have unclosed brackets");
1019    }
1020    if expected.contains('}') && matches!(found, Token::RBracket | Token::RParen) {
1021        diag = diag.with_note("you may have unclosed braces");
1022    }
1023
1024    // Common mistake: using -> instead of =>
1025    if expected.contains("=>") && matches!(found, Token::Arrow) {
1026        diag = diag
1027            .with_note("use `=>` for match arms, `->` is for return types")
1028            .with_suggestion("use fat arrow for match arm", span, "=>");
1029    }
1030
1031    // Common mistake: using => instead of ->
1032    if expected.contains("->") && matches!(found, Token::FatArrow) {
1033        diag = diag
1034            .with_note("use `->` for return types, `=>` is for match arms")
1035            .with_suggestion("use thin arrow for return type", span, "->");
1036    }
1037
1038    diag
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use super::*;
1044
1045    #[test]
1046    fn test_undefined_variable_suggestion() {
1047        let known = vec!["counter", "count", "total", "sum"];
1048        let diag = DiagnosticBuilder::undefined_variable("countr", Span::new(10, 16), &known);
1049
1050        assert!(diag.suggestions.iter().any(|s| s.replacement == "counter"));
1051    }
1052
1053    #[test]
1054    fn test_evidentiality_mismatch() {
1055        let diag = DiagnosticBuilder::evidentiality_mismatch("!", "~", Span::new(0, 5));
1056
1057        assert!(diag.notes.iter().any(|n| n.contains("validate")));
1058    }
1059
1060    #[test]
1061    fn test_unicode_symbol_suggestions() {
1062        // Logic operators
1063        assert_eq!(
1064            DiagnosticBuilder::suggest_unicode_symbol("&&"),
1065            Some(("∧", "logical AND"))
1066        );
1067        assert_eq!(
1068            DiagnosticBuilder::suggest_unicode_symbol("||"),
1069            Some(("∨", "logical OR"))
1070        );
1071
1072        // Bitwise operators
1073        assert_eq!(
1074            DiagnosticBuilder::suggest_unicode_symbol("&"),
1075            Some(("⋏", "bitwise AND"))
1076        );
1077        assert_eq!(
1078            DiagnosticBuilder::suggest_unicode_symbol("|"),
1079            Some(("⋎", "bitwise OR"))
1080        );
1081
1082        // Morphemes
1083        assert_eq!(
1084            DiagnosticBuilder::suggest_unicode_symbol("tau"),
1085            Some(("τ", "transform morpheme"))
1086        );
1087        assert_eq!(
1088            DiagnosticBuilder::suggest_unicode_symbol("filter"),
1089            Some(("φ", "filter morpheme"))
1090        );
1091        assert_eq!(
1092            DiagnosticBuilder::suggest_unicode_symbol("alpha"),
1093            Some(("α", "first element"))
1094        );
1095
1096        // Math
1097        assert_eq!(
1098            DiagnosticBuilder::suggest_unicode_symbol("sqrt"),
1099            Some(("√", "square root"))
1100        );
1101        assert_eq!(
1102            DiagnosticBuilder::suggest_unicode_symbol("infinity"),
1103            Some(("∞", "infinity"))
1104        );
1105
1106        // Unknown
1107        assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("foobar"), None);
1108    }
1109
1110    #[test]
1111    fn test_symbol_upgrade_diagnostic() {
1112        let diag = DiagnosticBuilder::suggest_symbol_upgrade("&&", Span::new(0, 2));
1113        assert!(diag.is_some());
1114
1115        let d = diag.unwrap();
1116        assert!(d.suggestions.iter().any(|s| s.replacement == "∧"));
1117        assert!(d.notes.iter().any(|n| n.contains("logical AND")));
1118    }
1119
1120    #[test]
1121    fn test_unknown_morpheme_with_access_morphemes() {
1122        let diag = DiagnosticBuilder::unknown_morpheme("alph", Span::new(0, 4));
1123
1124        // Should suggest alpha
1125        assert!(diag.suggestions.iter().any(|s| s.replacement == "α"));
1126        // Should have notes about both transform and access morphemes
1127        assert!(diag.notes.iter().any(|n| n.contains("transform morphemes")));
1128        assert!(diag.notes.iter().any(|n| n.contains("access morphemes")));
1129    }
1130
1131    #[test]
1132    fn test_all_symbol_mappings() {
1133        let mappings = DiagnosticBuilder::all_symbol_mappings();
1134        assert!(!mappings.is_empty());
1135
1136        // Check that essential mappings exist
1137        assert!(mappings.iter().any(|(a, u, _)| *a == "&&" && *u == "∧"));
1138        assert!(mappings.iter().any(|(a, u, _)| *a == "tau" && *u == "τ"));
1139        assert!(mappings.iter().any(|(a, u, _)| *a == "sqrt" && *u == "√"));
1140    }
1141}