fusabi_frontend/
error.rs

1//! Error reporting for type checking and compilation.
2//!
3//! This module provides comprehensive error types with beautiful formatting,
4//! source location tracking, and helpful suggestions for fixing common mistakes.
5//!
6//! # Example
7//!
8//! ```rust
9//! use fusabi_frontend::error::{TypeError, TypeErrorKind};
10//! use fusabi_frontend::types::Type;
11//! use fusabi_frontend::span::{Span, Position};
12//!
13//! let err = TypeError::with_span(
14//!     TypeErrorKind::Mismatch {
15//!         expected: Type::Int,
16//!         got: Type::String,
17//!     },
18//!     Span::new(Position::new(1, 5, 4), Position::new(1, 10, 9))
19//! );
20//!
21//! let source = "let x = \"hello\"";
22//! println!("{}", err.format(source));
23//! ```
24
25use crate::span::Span;
26use crate::types::{Type, TypeVar};
27use std::fmt;
28
29/// Type error with context and location information.
30#[derive(Debug, Clone)]
31pub struct TypeError {
32    /// The kind of type error
33    pub kind: TypeErrorKind,
34    /// Source location where the error occurred
35    pub span: Option<Span>,
36    /// Context stack for nested errors
37    pub context: Vec<String>,
38}
39
40/// Different kinds of type errors.
41#[derive(Debug, Clone)]
42pub enum TypeErrorKind {
43    /// Type mismatch between expected and actual types
44    Mismatch {
45        /// The expected type
46        expected: Type,
47        /// The actual type found
48        got: Type,
49    },
50
51    /// Occurs check failed (infinite type)
52    OccursCheck {
53        /// The type variable
54        var: TypeVar,
55        /// The type it appears in
56        in_type: Type,
57    },
58
59    /// Unbound variable reference
60    UnboundVariable {
61        /// Name of the unbound variable
62        name: String,
63    },
64
65    /// Record field not found
66    FieldNotFound {
67        /// The record type
68        record_type: Type,
69        /// The field name that wasn't found
70        field: String,
71    },
72
73    /// Function arity mismatch
74    ArityMismatch {
75        /// Expected number of arguments
76        expected: usize,
77        /// Actual number of arguments provided
78        got: usize,
79    },
80
81    /// Attempted to call a non-function
82    NotAFunction {
83        /// The type that was called
84        got: Type,
85    },
86
87    /// Pattern match type mismatch
88    PatternMismatch {
89        /// Type of the pattern
90        pattern_type: Type,
91        /// Type of the scrutinee
92        scrutinee_type: Type,
93    },
94
95    /// Not a tuple (but tuple access was attempted)
96    NotATuple {
97        /// The type that was accessed
98        got: Type,
99    },
100
101    /// Tuple index out of bounds
102    TupleIndexOutOfBounds {
103        /// The tuple type
104        tuple_type: Type,
105        /// The index that was accessed
106        index: usize,
107        /// The actual tuple size
108        size: usize,
109    },
110
111    /// Not a list (but list operation was attempted)
112    NotAList {
113        /// The type that was used
114        got: Type,
115    },
116
117    /// Not an array (but array operation was attempted)
118    NotAnArray {
119        /// The type that was used
120        got: Type,
121    },
122
123    /// Not a record (but record operation was attempted)
124    NotARecord {
125        /// The type that was used
126        got: Type,
127    },
128
129    /// Duplicate field in record
130    DuplicateField {
131        /// The field name
132        field: String,
133    },
134
135    /// Missing field in record
136    MissingField {
137        /// The record type
138        record_type: Type,
139        /// The missing field name
140        field: String,
141    },
142
143    /// Generic error with custom message
144    Custom {
145        /// Error message
146        message: String,
147    },
148}
149
150impl TypeError {
151    /// Create a new type error without location information.
152    pub fn new(kind: TypeErrorKind) -> Self {
153        TypeError {
154            kind,
155            span: None,
156            context: Vec::new(),
157        }
158    }
159
160    /// Create a new type error with span information.
161    pub fn with_span(kind: TypeErrorKind, span: Span) -> Self {
162        TypeError {
163            kind,
164            span: Some(span),
165            context: Vec::new(),
166        }
167    }
168
169    /// Add context to the error (e.g., "in function application", "in let binding").
170    pub fn add_context(&mut self, ctx: String) {
171        self.context.push(ctx);
172    }
173
174    /// Add context and return self (builder pattern).
175    pub fn with_context(mut self, ctx: String) -> Self {
176        self.add_context(ctx);
177        self
178    }
179
180    /// Format the error with source code highlighting.
181    ///
182    /// Produces a beautiful error message with:
183    /// - Error description
184    /// - Source location
185    /// - Source code excerpt with highlighting
186    /// - Context stack
187    /// - Helpful suggestions
188    pub fn format(&self, source: &str) -> String {
189        let mut output = String::new();
190
191        // Error header
192        output.push_str(&format!("Error: {}\n", self));
193
194        // Location if available
195        if let Some(span) = &self.span {
196            output.push_str(&format!("  --> {}\n", span.format_location()));
197
198            // Show source line with highlight
199            if let Some(highlight) = self.format_source_highlight(source, span) {
200                output.push_str(&highlight);
201            }
202        }
203
204        // Context stack
205        if !self.context.is_empty() {
206            output.push('\n');
207            for ctx in &self.context {
208                output.push_str(&format!("  in {}\n", ctx));
209            }
210        }
211
212        // Suggestions (if applicable)
213        if let Some(suggestion) = self.suggest_fix() {
214            output.push_str(&format!("\n  Help: {}\n", suggestion));
215        }
216
217        output
218    }
219
220    /// Format source code with error highlighting.
221    fn format_source_highlight(&self, source: &str, span: &Span) -> Option<String> {
222        let lines: Vec<&str> = source.lines().collect();
223        if span.start.line == 0 || span.start.line > lines.len() {
224            return None;
225        }
226
227        let line_idx = span.start.line - 1;
228        let line = lines[line_idx];
229
230        let mut output = String::new();
231        output.push_str("   |\n");
232        output.push_str(&format!("{:3} | {}\n", span.start.line, line));
233        output.push_str("   | ");
234
235        // Highlight the error span
236        let start_col = span.start.column.saturating_sub(1);
237        let end_col = if span.is_single_line() {
238            span.end.column.saturating_sub(1)
239        } else {
240            line.len()
241        };
242
243        for i in 0..line.len() {
244            if i >= start_col && i < end_col {
245                output.push('^');
246            } else {
247                output.push(' ');
248            }
249        }
250        output.push('\n');
251
252        Some(output)
253    }
254
255    /// Suggest a fix for common error patterns.
256    pub fn suggest_fix(&self) -> Option<String> {
257        match &self.kind {
258            TypeErrorKind::UnboundVariable { name } => {
259                Some(format!("Did you forget to define '{}'?", name))
260            }
261            TypeErrorKind::Mismatch { expected, got } => {
262                // Type-specific suggestions
263                match (expected, got) {
264                    (Type::Int, Type::String) | (Type::Int, Type::Float) => {
265                        Some("Try using a numeric conversion function".to_string())
266                    }
267                    (Type::String, Type::Int) | (Type::String, Type::Float) => {
268                        Some("Try using string conversion or formatting".to_string())
269                    }
270                    (Type::List(_), Type::Array(_)) | (Type::Array(_), Type::List(_)) => {
271                        Some("Lists and arrays are different types - use appropriate conversion functions".to_string())
272                    }
273                    (Type::Function(_, _), _) => {
274                        Some("Did you forget to apply all function arguments?".to_string())
275                    }
276                    (_, Type::Function(_, _)) => {
277                        Some("Did you provide too many arguments?".to_string())
278                    }
279                    _ => None,
280                }
281            }
282            TypeErrorKind::NotAFunction { got } => match got {
283                Type::Int | Type::Bool | Type::String | Type::Unit | Type::Float => {
284                    Some("This is a value, not a function - did you mean to call a function instead?".to_string())
285                }
286                _ => Some("This expression is not a function and cannot be called".to_string()),
287            },
288            TypeErrorKind::FieldNotFound { record_type, field: _ } => {
289                if let Type::Record(fields) = record_type {
290                    let available: Vec<_> = fields.keys().map(|s| s.as_str()).collect();
291                    Some(format!(
292                        "Available fields are: {}. Did you mean one of these?",
293                        available.join(", ")
294                    ))
295                } else {
296                    None
297                }
298            }
299            TypeErrorKind::ArityMismatch { expected, got } => {
300                if got < expected {
301                    Some(format!("You provided {} arguments but {} are required", got, expected))
302                } else {
303                    Some(format!("You provided {} arguments but only {} are expected", got, expected))
304                }
305            }
306            TypeErrorKind::TupleIndexOutOfBounds { size, index, .. } => {
307                Some(format!(
308                    "Tuple has {} elements (valid indices: 0-{}), but you tried to access index {}",
309                    size,
310                    size - 1,
311                    index
312                ))
313            }
314            TypeErrorKind::NotATuple { .. } => {
315                Some("Use tuple syntax (x, y, z) to create tuples".to_string())
316            }
317            TypeErrorKind::NotAList { .. } => {
318                Some("Use list syntax [x; y; z] to create lists".to_string())
319            }
320            TypeErrorKind::NotAnArray { .. } => {
321                Some("Use array syntax [|x; y; z|] to create arrays".to_string())
322            }
323            TypeErrorKind::NotARecord { .. } => {
324                Some("Use record syntax { field = value } to create records".to_string())
325            }
326            TypeErrorKind::DuplicateField { field } => {
327                Some(format!("Remove the duplicate definition of field '{}'", field))
328            }
329            TypeErrorKind::MissingField { field, .. } => {
330                Some(format!("Add the required field '{}' to the record", field))
331            }
332            TypeErrorKind::OccursCheck { var, in_type } => {
333                Some(format!(
334                    "This would create an infinite type {} = {}. Check your recursive type definitions.",
335                    var, in_type
336                ))
337            }
338            TypeErrorKind::PatternMismatch { .. } => {
339                Some("Pattern type must match the type of the value being matched".to_string())
340            }
341            TypeErrorKind::Custom { .. } => None,
342        }
343    }
344}
345
346impl fmt::Display for TypeError {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match &self.kind {
349            TypeErrorKind::Mismatch { expected, got } => {
350                writeln!(f, "Type mismatch")?;
351                writeln!(f, "  Expected: {}", expected)?;
352                write!(f, "  Got:      {}", got)
353            }
354            TypeErrorKind::OccursCheck { var, in_type } => {
355                writeln!(f, "Occurs check failed")?;
356                write!(f, "  Cannot construct infinite type {} = {}", var, in_type)
357            }
358            TypeErrorKind::UnboundVariable { name } => {
359                write!(f, "Unbound variable: {}", name)
360            }
361            TypeErrorKind::FieldNotFound { record_type, field } => {
362                write!(
363                    f,
364                    "Field '{}' not found in record type {}",
365                    field, record_type
366                )
367            }
368            TypeErrorKind::ArityMismatch { expected, got } => {
369                write!(
370                    f,
371                    "Arity mismatch: expected {} arguments, got {}",
372                    expected, got
373                )
374            }
375            TypeErrorKind::NotAFunction { got } => {
376                write!(f, "Not a function: cannot call value of type {}", got)
377            }
378            TypeErrorKind::PatternMismatch {
379                pattern_type,
380                scrutinee_type,
381            } => {
382                writeln!(f, "Pattern match type mismatch")?;
383                writeln!(f, "  Pattern type:   {}", pattern_type)?;
384                write!(f, "  Scrutinee type: {}", scrutinee_type)
385            }
386            TypeErrorKind::NotATuple { got } => {
387                write!(
388                    f,
389                    "Not a tuple: cannot access tuple element of type {}",
390                    got
391                )
392            }
393            TypeErrorKind::TupleIndexOutOfBounds {
394                tuple_type,
395                index,
396                size,
397            } => {
398                write!(
399                    f,
400                    "Tuple index out of bounds: type {} has {} elements, but index {} was accessed",
401                    tuple_type, size, index
402                )
403            }
404            TypeErrorKind::NotAList { got } => {
405                write!(
406                    f,
407                    "Not a list: cannot perform list operation on type {}",
408                    got
409                )
410            }
411            TypeErrorKind::NotAnArray { got } => {
412                write!(
413                    f,
414                    "Not an array: cannot perform array operation on type {}",
415                    got
416                )
417            }
418            TypeErrorKind::NotARecord { got } => {
419                write!(f, "Not a record: cannot access field of type {}", got)
420            }
421            TypeErrorKind::DuplicateField { field } => {
422                write!(f, "Duplicate field in record: '{}'", field)
423            }
424            TypeErrorKind::MissingField { record_type, field } => {
425                write!(
426                    f,
427                    "Missing field '{}' in record type {}",
428                    field, record_type
429                )
430            }
431            TypeErrorKind::Custom { message } => {
432                write!(f, "{}", message)
433            }
434        }
435    }
436}
437
438impl std::error::Error for TypeError {}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use crate::span::Position;
444    use crate::types::TypeVar;
445
446    // ========================================================================
447    // TypeError Creation Tests
448    // ========================================================================
449
450    #[test]
451    fn test_type_error_new() {
452        let err = TypeError::new(TypeErrorKind::UnboundVariable {
453            name: "x".to_string(),
454        });
455        assert!(err.span.is_none());
456        assert!(err.context.is_empty());
457    }
458
459    #[test]
460    fn test_type_error_with_span() {
461        let span = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
462        let err = TypeError::with_span(
463            TypeErrorKind::UnboundVariable {
464                name: "x".to_string(),
465            },
466            span,
467        );
468        assert_eq!(err.span, Some(span));
469    }
470
471    #[test]
472    fn test_type_error_add_context() {
473        let mut err = TypeError::new(TypeErrorKind::UnboundVariable {
474            name: "x".to_string(),
475        });
476        err.add_context("function application".to_string());
477        err.add_context("let binding".to_string());
478        assert_eq!(err.context.len(), 2);
479        assert_eq!(err.context[0], "function application");
480        assert_eq!(err.context[1], "let binding");
481    }
482
483    #[test]
484    fn test_type_error_with_context_builder() {
485        let err = TypeError::new(TypeErrorKind::UnboundVariable {
486            name: "x".to_string(),
487        })
488        .with_context("function application".to_string());
489        assert_eq!(err.context.len(), 1);
490    }
491
492    // ========================================================================
493    // TypeErrorKind Display Tests
494    // ========================================================================
495
496    #[test]
497    fn test_display_type_mismatch() {
498        let err = TypeError::new(TypeErrorKind::Mismatch {
499            expected: Type::Int,
500            got: Type::String,
501        });
502        let display = format!("{}", err);
503        assert!(display.contains("Type mismatch"));
504        assert!(display.contains("Expected: int"));
505        assert!(display.contains("Got:      string"));
506    }
507
508    #[test]
509    fn test_display_occurs_check() {
510        let var = TypeVar::new(0, "a");
511        let err = TypeError::new(TypeErrorKind::OccursCheck {
512            var: var.clone(),
513            in_type: Type::List(Box::new(Type::Var(var))),
514        });
515        let display = format!("{}", err);
516        assert!(display.contains("Occurs check failed"));
517        assert!(display.contains("infinite type"));
518    }
519
520    #[test]
521    fn test_display_unbound_variable() {
522        let err = TypeError::new(TypeErrorKind::UnboundVariable {
523            name: "foo".to_string(),
524        });
525        let display = format!("{}", err);
526        assert!(display.contains("Unbound variable: foo"));
527    }
528
529    #[test]
530    fn test_display_field_not_found() {
531        use std::collections::HashMap;
532        let mut fields = HashMap::new();
533        fields.insert("x".to_string(), Type::Int);
534        let record = Type::Record(fields);
535
536        let err = TypeError::new(TypeErrorKind::FieldNotFound {
537            record_type: record,
538            field: "y".to_string(),
539        });
540        let display = format!("{}", err);
541        assert!(display.contains("Field 'y' not found"));
542    }
543
544    #[test]
545    fn test_display_arity_mismatch() {
546        let err = TypeError::new(TypeErrorKind::ArityMismatch {
547            expected: 2,
548            got: 3,
549        });
550        let display = format!("{}", err);
551        assert!(display.contains("Arity mismatch"));
552        assert!(display.contains("expected 2"));
553        assert!(display.contains("got 3"));
554    }
555
556    #[test]
557    fn test_display_not_a_function() {
558        let err = TypeError::new(TypeErrorKind::NotAFunction { got: Type::Int });
559        let display = format!("{}", err);
560        assert!(display.contains("Not a function"));
561        assert!(display.contains("int"));
562    }
563
564    #[test]
565    fn test_display_pattern_mismatch() {
566        let err = TypeError::new(TypeErrorKind::PatternMismatch {
567            pattern_type: Type::Int,
568            scrutinee_type: Type::String,
569        });
570        let display = format!("{}", err);
571        assert!(display.contains("Pattern match type mismatch"));
572    }
573
574    #[test]
575    fn test_display_not_a_tuple() {
576        let err = TypeError::new(TypeErrorKind::NotATuple { got: Type::Int });
577        let display = format!("{}", err);
578        assert!(display.contains("Not a tuple"));
579    }
580
581    #[test]
582    fn test_display_tuple_index_out_of_bounds() {
583        let tuple = Type::Tuple(vec![Type::Int, Type::Bool]);
584        let err = TypeError::new(TypeErrorKind::TupleIndexOutOfBounds {
585            tuple_type: tuple,
586            index: 3,
587            size: 2,
588        });
589        let display = format!("{}", err);
590        assert!(display.contains("Tuple index out of bounds"));
591        assert!(display.contains("index 3"));
592    }
593
594    #[test]
595    fn test_display_not_a_list() {
596        let err = TypeError::new(TypeErrorKind::NotAList { got: Type::Int });
597        let display = format!("{}", err);
598        assert!(display.contains("Not a list"));
599    }
600
601    #[test]
602    fn test_display_not_an_array() {
603        let err = TypeError::new(TypeErrorKind::NotAnArray { got: Type::Int });
604        let display = format!("{}", err);
605        assert!(display.contains("Not an array"));
606    }
607
608    #[test]
609    fn test_display_duplicate_field() {
610        let err = TypeError::new(TypeErrorKind::DuplicateField {
611            field: "name".to_string(),
612        });
613        let display = format!("{}", err);
614        assert!(display.contains("Duplicate field"));
615        assert!(display.contains("name"));
616    }
617
618    #[test]
619    fn test_display_custom() {
620        let err = TypeError::new(TypeErrorKind::Custom {
621            message: "Something went wrong".to_string(),
622        });
623        let display = format!("{}", err);
624        assert_eq!(display, "Something went wrong");
625    }
626
627    // ========================================================================
628    // Suggestion Tests
629    // ========================================================================
630
631    #[test]
632    fn test_suggestion_unbound_variable() {
633        let err = TypeError::new(TypeErrorKind::UnboundVariable {
634            name: "foo".to_string(),
635        });
636        let suggestion = err.suggest_fix();
637        assert!(suggestion.is_some());
638        assert!(suggestion.unwrap().contains("Did you forget to define"));
639    }
640
641    #[test]
642    fn test_suggestion_int_to_string_mismatch() {
643        let err = TypeError::new(TypeErrorKind::Mismatch {
644            expected: Type::Int,
645            got: Type::String,
646        });
647        let suggestion = err.suggest_fix();
648        assert!(suggestion.is_some());
649        assert!(suggestion.unwrap().contains("conversion"));
650    }
651
652    #[test]
653    fn test_suggestion_not_a_function() {
654        let err = TypeError::new(TypeErrorKind::NotAFunction { got: Type::Int });
655        let suggestion = err.suggest_fix();
656        assert!(suggestion.is_some());
657    }
658
659    // ========================================================================
660    // Source Formatting Tests
661    // ========================================================================
662
663    #[test]
664    fn test_format_with_source_simple() {
665        let source = "let x = 42";
666        let span = Span::new(Position::new(1, 5, 4), Position::new(1, 6, 5));
667        let err = TypeError::with_span(
668            TypeErrorKind::UnboundVariable {
669                name: "x".to_string(),
670            },
671            span,
672        );
673        let formatted = err.format(source);
674        assert!(formatted.contains("Error:"));
675        assert!(formatted.contains("line 1, column 5"));
676        assert!(formatted.contains("let x = 42"));
677    }
678
679    #[test]
680    fn test_format_with_context() {
681        let source = "let x = 42";
682        let span = Span::new(Position::new(1, 1, 0), Position::new(1, 3, 2));
683        let err = TypeError::with_span(
684            TypeErrorKind::Mismatch {
685                expected: Type::Int,
686                got: Type::String,
687            },
688            span,
689        )
690        .with_context("function application".to_string());
691
692        let formatted = err.format(source);
693        assert!(formatted.contains("in function application"));
694    }
695
696    #[test]
697    fn test_format_with_suggestion() {
698        let source = "let x = y";
699        let span = Span::new(Position::new(1, 9, 8), Position::new(1, 10, 9));
700        let err = TypeError::with_span(
701            TypeErrorKind::UnboundVariable {
702                name: "y".to_string(),
703            },
704            span,
705        );
706        let formatted = err.format(source);
707        assert!(formatted.contains("Help:"));
708        assert!(formatted.contains("Did you forget to define"));
709    }
710
711    #[test]
712    fn test_format_source_highlight() {
713        let source = "let x = 42";
714        let span = Span::new(Position::new(1, 9, 8), Position::new(1, 11, 10));
715        let err = TypeError::with_span(
716            TypeErrorKind::Mismatch {
717                expected: Type::String,
718                got: Type::Int,
719            },
720            span,
721        );
722        let formatted = err.format(source);
723        assert!(formatted.contains("let x = 42"));
724        assert!(formatted.contains("^^"));
725    }
726}