Skip to main content

logicaffeine_language/
error.rs

1//! Error types and display formatting for parse errors.
2//!
3//! This module provides structured error types for the lexer and parser,
4//! with rich diagnostic output including:
5//!
6//! - Source location with line/column numbers
7//! - Syntax-highlighted error messages
8//! - Socratic explanations for common mistakes
9//! - Spelling suggestions for unknown words
10//!
11//! # Error Display
12//!
13//! Errors can be displayed with source context using [`ParseError::display_with_source`],
14//! which produces rustc-style error output with underlined spans.
15
16use logicaffeine_base::Interner;
17use crate::style::Style;
18use crate::suggest::{find_similar, KNOWN_WORDS};
19use crate::token::{Span, TokenType};
20
21/// A parse error with location information.
22#[derive(Debug, Clone)]
23pub struct ParseError {
24    pub kind: ParseErrorKind,
25    pub span: Span,
26}
27
28impl ParseError {
29    pub fn display_with_source(&self, source: &str) -> String {
30        let (line_num, line_start, line_content) = self.find_context(source);
31        let col = self.span.start.saturating_sub(line_start);
32        let len = (self.span.end - self.span.start).max(1);
33        let underline = format!("{}{}", " ".repeat(col), "^".repeat(len));
34
35        let error_label = Style::bold_red("error");
36        let kind_str = format!("{:?}", self.kind);
37        let line_num_str = Style::blue(&format!("{:4}", line_num));
38        let pipe = Style::blue("|");
39        let underline_colored = Style::red(&underline);
40
41        let mut result = format!(
42            "{}: {}\n\n{} {} {}\n     {} {}",
43            error_label, kind_str, line_num_str, pipe, line_content, pipe, underline_colored
44        );
45
46        if let Some(word) = self.extract_word(source) {
47            if let Some(suggestion) = find_similar(&word, KNOWN_WORDS, 2) {
48                let hint = Style::cyan("help");
49                result.push_str(&format!("\n     {} {}: did you mean '{}'?", pipe, hint, Style::green(suggestion)));
50            }
51        }
52
53        result
54    }
55
56    fn extract_word<'a>(&self, source: &'a str) -> Option<&'a str> {
57        if self.span.start < source.len() && self.span.end <= source.len() {
58            let word = &source[self.span.start..self.span.end];
59            if !word.is_empty() && word.chars().all(|c| c.is_alphabetic()) {
60                return Some(word);
61            }
62        }
63        None
64    }
65
66    fn find_context<'a>(&self, source: &'a str) -> (usize, usize, &'a str) {
67        let mut line_num = 1;
68        let mut line_start = 0;
69
70        for (i, c) in source.char_indices() {
71            if i >= self.span.start {
72                break;
73            }
74            if c == '\n' {
75                line_num += 1;
76                line_start = i + 1;
77            }
78        }
79
80        let line_end = source[line_start..]
81            .find('\n')
82            .map(|off| line_start + off)
83            .unwrap_or(source.len());
84
85        (line_num, line_start, &source[line_start..line_end])
86    }
87}
88
89#[derive(Debug, Clone)]
90pub enum ParseErrorKind {
91    UnexpectedToken {
92        expected: TokenType,
93        found: TokenType,
94    },
95    ExpectedContentWord {
96        found: TokenType,
97    },
98    ExpectedCopula,
99    UnknownQuantifier {
100        found: TokenType,
101    },
102    UnknownModal {
103        found: TokenType,
104    },
105    ExpectedVerb {
106        found: TokenType,
107    },
108    ExpectedTemporalAdverb,
109    ExpectedPresuppositionTrigger,
110    ExpectedFocusParticle,
111    ExpectedScopalAdverb,
112    ExpectedSuperlativeAdjective,
113    ExpectedComparativeAdjective,
114    ExpectedThan,
115    ExpectedNumber,
116    EmptyRestriction,
117    GappingResolutionFailed,
118    StativeProgressiveConflict,
119    UndefinedVariable {
120        name: String,
121    },
122    UseAfterMove {
123        name: String,
124    },
125    IsValueEquality {
126        variable: String,
127        value: String,
128    },
129    ZeroIndex,
130    ExpectedStatement,
131    ExpectedKeyword { keyword: String },
132    ExpectedExpression,
133    ExpectedIdentifier,
134    /// Subject and object lists have different lengths in a "respectively" construction.
135    RespectivelyLengthMismatch {
136        subject_count: usize,
137        object_count: usize,
138    },
139    /// Type mismatch during static type checking.
140    TypeMismatch {
141        expected: String,
142        found: String,
143    },
144    /// Type mismatch with context (e.g., "in argument 2 of 'compute'").
145    TypeMismatchDetailed {
146        expected: String,
147        found: String,
148        context: String,
149    },
150    /// A type variable would occur in its own definition (e.g., `T = List<T>`).
151    InfiniteType {
152        var_description: String,
153        type_description: String,
154    },
155    /// Wrong number of arguments in a function call.
156    ArityMismatch {
157        function: String,
158        expected: usize,
159        found: usize,
160    },
161    /// A field name does not exist on the struct type.
162    FieldNotFound {
163        type_name: String,
164        field_name: String,
165        available: Vec<String>,
166    },
167    /// Tried to call something that is not a function.
168    NotAFunction {
169        found_type: String,
170    },
171    /// Invalid refinement predicate in a dependent type.
172    InvalidRefinementPredicate,
173    /// Grammar error (e.g., "its" vs "it's").
174    GrammarError(String),
175    /// DRS scope violation (pronoun trapped in negation, disjunction, etc.).
176    ScopeViolation(String),
177    /// Unresolved pronoun in discourse mode - no accessible antecedent found.
178    UnresolvedPronoun {
179        gender: crate::drs::Gender,
180        number: crate::drs::Number,
181    },
182    /// Custom error message (used for escape analysis, zone errors, etc.).
183    Custom(String),
184}
185
186#[cold]
187pub fn socratic_explanation(error: &ParseError, _interner: &Interner) -> String {
188    let pos = error.span.start;
189    match &error.kind {
190        ParseErrorKind::UnexpectedToken { expected, found } => {
191            format!(
192                "I was following your logic, but I stumbled at position {}. \
193                I expected {:?}, but found {:?}. Perhaps you meant to use a different word here?",
194                pos, expected, found
195            )
196        }
197        ParseErrorKind::ExpectedContentWord { found } => {
198            format!(
199                "I was looking for a noun, verb, or adjective at position {}, \
200                but found {:?} instead. The logic needs a content word to ground it.",
201                pos, found
202            )
203        }
204        ParseErrorKind::ExpectedCopula => {
205            format!(
206                "At position {}, I expected 'is' or 'are' to link the subject and predicate. \
207                Without it, the sentence structure is incomplete.",
208                pos
209            )
210        }
211        ParseErrorKind::UnknownQuantifier { found } => {
212            format!(
213                "At position {}, I found {:?} where I expected a quantifier like 'all', 'some', or 'no'. \
214                These words tell me how many things we're talking about.",
215                pos, found
216            )
217        }
218        ParseErrorKind::UnknownModal { found } => {
219            format!(
220                "At position {}, I found {:?} where I expected a modal like 'must', 'can', or 'should'. \
221                Modals express possibility, necessity, or obligation.",
222                pos, found
223            )
224        }
225        ParseErrorKind::ExpectedVerb { found } => {
226            format!(
227                "At position {}, I expected a verb to describe an action or state, \
228                but found {:?}. Every sentence needs a verb.",
229                pos, found
230            )
231        }
232        ParseErrorKind::ExpectedTemporalAdverb => {
233            format!(
234                "At position {}, I expected a temporal adverb like 'yesterday' or 'tomorrow' \
235                to anchor the sentence in time.",
236                pos
237            )
238        }
239        ParseErrorKind::ExpectedPresuppositionTrigger => {
240            format!(
241                "At position {}, I expected a presupposition trigger like 'stopped', 'realized', or 'regrets'. \
242                These words carry hidden assumptions.",
243                pos
244            )
245        }
246        ParseErrorKind::ExpectedFocusParticle => {
247            format!(
248                "At position {}, I expected a focus particle like 'only', 'even', or 'just'. \
249                These words highlight what's important in the sentence.",
250                pos
251            )
252        }
253        ParseErrorKind::ExpectedScopalAdverb => {
254            format!(
255                "At position {}, I expected a scopal adverb that modifies the entire proposition.",
256                pos
257            )
258        }
259        ParseErrorKind::ExpectedSuperlativeAdjective => {
260            format!(
261                "At position {}, I expected a superlative adjective like 'tallest' or 'fastest'. \
262                These words compare one thing to all others.",
263                pos
264            )
265        }
266        ParseErrorKind::ExpectedComparativeAdjective => {
267            format!(
268                "At position {}, I expected a comparative adjective like 'taller' or 'faster'. \
269                These words compare two things.",
270                pos
271            )
272        }
273        ParseErrorKind::ExpectedThan => {
274            format!(
275                "At position {}, I expected 'than' after the comparative. \
276                Comparisons need 'than' to introduce the thing being compared to.",
277                pos
278            )
279        }
280        ParseErrorKind::ExpectedNumber => {
281            format!(
282                "At position {}, I expected a numeric value like '2', '3.14', or 'aleph_0'. \
283                Measure phrases require a number.",
284                pos
285            )
286        }
287        ParseErrorKind::EmptyRestriction => {
288            format!(
289                "At position {}, the restriction clause is empty. \
290                A relative clause needs content to restrict the noun phrase.",
291                pos
292            )
293        }
294        ParseErrorKind::GappingResolutionFailed => {
295            format!(
296                "At position {}, I see a gapped construction (like '...and Mary, a pear'), \
297                but I couldn't find a verb in the previous clause to borrow. \
298                Gapping requires a clear action to repeat.",
299                pos
300            )
301        }
302        ParseErrorKind::StativeProgressiveConflict => {
303            format!(
304                "At position {}, a stative verb like 'know' or 'love' cannot be used with progressive aspect. \
305                Stative verbs describe states, not activities in progress.",
306                pos
307            )
308        }
309        ParseErrorKind::UndefinedVariable { name } => {
310            format!(
311                "At position {}, I found '{}' but this variable has not been defined. \
312                In imperative mode, all variables must be declared before use.",
313                pos, name
314            )
315        }
316        ParseErrorKind::UseAfterMove { name } => {
317            format!(
318                "At position {}, I found '{}' but this value has been moved. \
319                Once a value is moved, it cannot be used again.",
320                pos, name
321            )
322        }
323        ParseErrorKind::IsValueEquality { variable, value } => {
324            format!(
325                "At position {}, I found '{} is {}' but 'is' is for type/predicate checks. \
326                For value equality, use '{} equals {}'.",
327                pos, variable, value, variable, value
328            )
329        }
330        ParseErrorKind::ZeroIndex => {
331            format!(
332                "At position {}, I found 'item 0' but indices in LOGOS start at 1. \
333                In English, 'the 1st item' is the first item, not the zeroth. \
334                Try 'item 1 of list' to get the first element.",
335                pos
336            )
337        }
338        ParseErrorKind::ExpectedStatement => {
339            format!(
340                "At position {}, I expected a statement like 'Let', 'Set', or 'Return'.",
341                pos
342            )
343        }
344        ParseErrorKind::ExpectedKeyword { keyword } => {
345            format!(
346                "At position {}, I expected the keyword '{}'.",
347                pos, keyword
348            )
349        }
350        ParseErrorKind::ExpectedExpression => {
351            format!(
352                "At position {}, I expected an expression (number, variable, or computation).",
353                pos
354            )
355        }
356        ParseErrorKind::ExpectedIdentifier => {
357            format!(
358                "At position {}, I expected an identifier (variable name).",
359                pos
360            )
361        }
362        ParseErrorKind::RespectivelyLengthMismatch { subject_count, object_count } => {
363            format!(
364                "At position {}, 'respectively' requires equal-length lists. \
365                The subject has {} element(s) and the object has {} element(s). \
366                Each subject must pair with exactly one object.",
367                pos, subject_count, object_count
368            )
369        }
370        ParseErrorKind::TypeMismatch { expected, found } => {
371            format!(
372                "At position {}, I expected a value of type '{}' but found '{}'. \
373                Types must match in LOGOS. Check that your value matches the declared type.",
374                pos, expected, found
375            )
376        }
377        ParseErrorKind::TypeMismatchDetailed { expected, found, context } => {
378            let ctx_note = if context.is_empty() {
379                String::new()
380            } else {
381                format!(" ({})", context)
382            };
383            format!(
384                "At position {}, I expected '{}' but found '{}'{}.  \
385                Check that the types are consistent — perhaps the annotation or the value needs adjusting.",
386                pos, expected, found, ctx_note
387            )
388        }
389        ParseErrorKind::InfiniteType { var_description, type_description } => {
390            format!(
391                "At position {}, I detected an infinite recursive type: {} would need to equal {}. \
392                A type cannot contain itself. Consider using a named record or a level of indirection.",
393                pos, var_description, type_description
394            )
395        }
396        ParseErrorKind::ArityMismatch { function, expected, found } => {
397            format!(
398                "At position {}, '{}' takes {} argument(s) but was called with {}. \
399                Check the function signature and ensure you pass the right number of values.",
400                pos, function, expected, found
401            )
402        }
403        ParseErrorKind::FieldNotFound { type_name, field_name, available } => {
404            let avail_note = if available.is_empty() {
405                String::new()
406            } else {
407                format!(" Available fields: {}.", available.join(", "))
408            };
409            format!(
410                "At position {}, '{}' has no field named '{}'.{} \
411                Check the spelling or use one of the declared fields.",
412                pos, type_name, field_name, avail_note
413            )
414        }
415        ParseErrorKind::NotAFunction { found_type } => {
416            format!(
417                "At position {}, I tried to call a value of type '{}' as a function. \
418                Only functions and closures can be called. \
419                Check that you are applying arguments to an actual function.",
420                pos, found_type
421            )
422        }
423        ParseErrorKind::InvalidRefinementPredicate => {
424            format!(
425                "At position {}, the refinement predicate is not valid. \
426                A refinement predicate must be a comparison like 'x > 0' or 'n < 100'.",
427                pos
428            )
429        }
430        ParseErrorKind::GrammarError(msg) => {
431            format!(
432                "At position {}, grammar issue: {}",
433                pos, msg
434            )
435        }
436        ParseErrorKind::ScopeViolation(msg) => {
437            format!(
438                "At position {}, scope violation: {}. The pronoun cannot access a referent \
439                trapped in a different scope (e.g., inside negation or disjunction).",
440                pos, msg
441            )
442        }
443        ParseErrorKind::UnresolvedPronoun { gender, number } => {
444            format!(
445                "At position {}, I found a {:?} {:?} pronoun but couldn't resolve it. \
446                In discourse mode, all pronouns must have an accessible antecedent from earlier sentences. \
447                The referent may be trapped in an inaccessible scope (negation, disjunction) or \
448                there may be no matching referent.",
449                pos, gender, number
450            )
451        }
452        ParseErrorKind::Custom(msg) => msg.clone(),
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::token::Span;
460
461    #[test]
462    fn parse_error_has_span() {
463        let error = ParseError {
464            kind: ParseErrorKind::ExpectedCopula,
465            span: Span::new(5, 10),
466        };
467        assert_eq!(error.span.start, 5);
468        assert_eq!(error.span.end, 10);
469    }
470
471    #[test]
472    fn display_with_source_shows_line_and_underline() {
473        let error = ParseError {
474            kind: ParseErrorKind::ExpectedCopula,
475            span: Span::new(8, 14),
476        };
477        let source = "All men mortal are.";
478        let display = error.display_with_source(source);
479        assert!(display.contains("mortal"), "Should contain source word: {}", display);
480        assert!(display.contains("^^^^^^"), "Should contain underline: {}", display);
481    }
482
483    #[test]
484    fn display_with_source_suggests_typo_fix() {
485        let error = ParseError {
486            kind: ParseErrorKind::ExpectedCopula,
487            span: Span::new(0, 5),
488        };
489        let source = "logoc is the study of reason.";
490        let display = error.display_with_source(source);
491        assert!(display.contains("did you mean"), "Should suggest fix: {}", display);
492        assert!(display.contains("logic"), "Should suggest 'logic': {}", display);
493    }
494
495    #[test]
496    fn display_with_source_has_color_codes() {
497        let error = ParseError {
498            kind: ParseErrorKind::ExpectedCopula,
499            span: Span::new(0, 3),
500        };
501        let source = "Alll men are mortal.";
502        let display = error.display_with_source(source);
503        assert!(display.contains("\x1b["), "Should contain ANSI escape codes: {}", display);
504    }
505
506    // =========================================================================
507    // Phase 4 — Type error reporting variants
508    // =========================================================================
509
510    #[test]
511    fn type_mismatch_detailed_socratic_mentions_types() {
512        let interner = logicaffeine_base::Interner::new();
513        let error = ParseError {
514            kind: ParseErrorKind::TypeMismatchDetailed {
515                expected: "Int".to_string(),
516                found: "Bool".to_string(),
517                context: "in let binding".to_string(),
518            },
519            span: Span::new(0, 0),
520        };
521        let explanation = socratic_explanation(&error, &interner);
522        assert!(explanation.contains("Int"), "Should mention expected type: {}", explanation);
523        assert!(explanation.contains("Bool"), "Should mention found type: {}", explanation);
524        assert!(explanation.contains("let binding"), "Should include context: {}", explanation);
525    }
526
527    #[test]
528    fn type_mismatch_detailed_without_context_is_clean() {
529        let interner = logicaffeine_base::Interner::new();
530        let error = ParseError {
531            kind: ParseErrorKind::TypeMismatchDetailed {
532                expected: "Text".to_string(),
533                found: "Int".to_string(),
534                context: String::new(),
535            },
536            span: Span::new(0, 0),
537        };
538        let explanation = socratic_explanation(&error, &interner);
539        assert!(explanation.contains("Text"), "Should mention expected type: {}", explanation);
540        assert!(explanation.contains("Int"), "Should mention found type: {}", explanation);
541        // No spurious "()" from empty context
542        assert!(!explanation.contains("()"), "Empty context should not leave '()': {}", explanation);
543    }
544
545    #[test]
546    fn infinite_type_socratic_mentions_both_descriptions() {
547        let interner = logicaffeine_base::Interner::new();
548        let error = ParseError {
549            kind: ParseErrorKind::InfiniteType {
550                var_description: "type variable α0".to_string(),
551                type_description: "Seq of α0".to_string(),
552            },
553            span: Span::new(0, 0),
554        };
555        let explanation = socratic_explanation(&error, &interner);
556        assert!(explanation.contains("α0"), "Should mention var: {}", explanation);
557        assert!(explanation.contains("Seq of α0"), "Should mention type: {}", explanation);
558    }
559
560    #[test]
561    fn arity_mismatch_socratic_mentions_function_and_counts() {
562        let interner = logicaffeine_base::Interner::new();
563        let error = ParseError {
564            kind: ParseErrorKind::ArityMismatch {
565                function: "double".to_string(),
566                expected: 1,
567                found: 3,
568            },
569            span: Span::new(0, 0),
570        };
571        let explanation = socratic_explanation(&error, &interner);
572        assert!(explanation.contains("double"), "Should name the function: {}", explanation);
573        assert!(explanation.contains("1"), "Should mention expected count: {}", explanation);
574        assert!(explanation.contains("3"), "Should mention found count: {}", explanation);
575    }
576
577    #[test]
578    fn field_not_found_socratic_mentions_type_and_field() {
579        let interner = logicaffeine_base::Interner::new();
580        let error = ParseError {
581            kind: ParseErrorKind::FieldNotFound {
582                type_name: "Point".to_string(),
583                field_name: "z".to_string(),
584                available: vec!["x".to_string(), "y".to_string()],
585            },
586            span: Span::new(0, 0),
587        };
588        let explanation = socratic_explanation(&error, &interner);
589        assert!(explanation.contains("Point"), "Should name the type: {}", explanation);
590        assert!(explanation.contains("z"), "Should name the missing field: {}", explanation);
591        assert!(explanation.contains("x"), "Should list available fields: {}", explanation);
592        assert!(explanation.contains("y"), "Should list available fields: {}", explanation);
593    }
594
595    #[test]
596    fn not_a_function_socratic_mentions_found_type() {
597        let interner = logicaffeine_base::Interner::new();
598        let error = ParseError {
599            kind: ParseErrorKind::NotAFunction {
600                found_type: "Int".to_string(),
601            },
602            span: Span::new(0, 0),
603        };
604        let explanation = socratic_explanation(&error, &interner);
605        assert!(explanation.contains("Int"), "Should mention the type found: {}", explanation);
606        assert!(explanation.to_lowercase().contains("function"), "Should mention function: {}", explanation);
607    }
608}