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
34        // Window very long lines around the error column — a generated
35        // 20,000-character expression must not flood the terminal with its
36        // own excerpt.
37        const EXCERPT_MAX: usize = 160;
38        let (line_content, col, len) = if line_content.chars().count() > EXCERPT_MAX {
39            let chars: Vec<char> = line_content.chars().collect();
40            let col_chars = line_content
41                .char_indices()
42                .take_while(|(i, _)| *i < col)
43                .count();
44            let from = col_chars.saturating_sub(EXCERPT_MAX / 2).min(chars.len());
45            let to = (from + EXCERPT_MAX).min(chars.len());
46            let mut windowed: String = chars[from..to].iter().collect();
47            if from > 0 {
48                windowed = format!("…{windowed}");
49            }
50            if to < chars.len() {
51                windowed.push('…');
52            }
53            let new_col = col_chars - from + usize::from(from > 0);
54            (
55                std::borrow::Cow::Owned(windowed),
56                new_col,
57                len.min(EXCERPT_MAX / 2),
58            )
59        } else {
60            (std::borrow::Cow::Borrowed(line_content), col, len)
61        };
62        let line_content: &str = &line_content;
63        let underline = format!("{}{}", " ".repeat(col), "^".repeat(len));
64
65        let error_label = Style::bold_red("error");
66        let kind_str = format!("{:?}", self.kind);
67        let line_num_str = Style::blue(&format!("{:4}", line_num));
68        let pipe = Style::blue("|");
69        let underline_colored = Style::red(&underline);
70
71        let mut result = format!(
72            "{}: {}\n\n{} {} {}\n     {} {}",
73            error_label, kind_str, line_num_str, pipe, line_content, pipe, underline_colored
74        );
75
76        if let Some(word) = self.extract_word(source) {
77            if let Some(suggestion) = find_similar(&word, KNOWN_WORDS, 2) {
78                let hint = Style::cyan("help");
79                result.push_str(&format!("\n     {} {}: did you mean '{}'?", pipe, hint, Style::green(suggestion)));
80            }
81        }
82
83        result
84    }
85
86    fn extract_word<'a>(&self, source: &'a str) -> Option<&'a str> {
87        if self.span.start < source.len() && self.span.end <= source.len() {
88            let word = &source[self.span.start..self.span.end];
89            if !word.is_empty() && word.chars().all(|c| c.is_alphabetic()) {
90                return Some(word);
91            }
92        }
93        None
94    }
95
96    fn find_context<'a>(&self, source: &'a str) -> (usize, usize, &'a str) {
97        let mut line_num = 1;
98        let mut line_start = 0;
99
100        for (i, c) in source.char_indices() {
101            if i >= self.span.start {
102                break;
103            }
104            if c == '\n' {
105                line_num += 1;
106                line_start = i + 1;
107            }
108        }
109
110        let line_end = source[line_start..]
111            .find('\n')
112            .map(|off| line_start + off)
113            .unwrap_or(source.len());
114
115        (line_num, line_start, &source[line_start..line_end])
116    }
117}
118
119#[derive(Debug, Clone)]
120pub enum ParseErrorKind {
121    UnexpectedToken {
122        expected: TokenType,
123        found: TokenType,
124    },
125    ExpectedContentWord {
126        found: TokenType,
127    },
128    ExpectedCopula,
129    UnknownQuantifier {
130        found: TokenType,
131    },
132    UnknownModal {
133        found: TokenType,
134    },
135    ExpectedVerb {
136        found: TokenType,
137    },
138    ExpectedTemporalAdverb,
139    ExpectedPresuppositionTrigger,
140    ExpectedFocusParticle,
141    ExpectedScopalAdverb,
142    ExpectedSuperlativeAdjective,
143    ExpectedComparativeAdjective,
144    ExpectedThan,
145    ExpectedNumber,
146    EmptyRestriction,
147    GappingResolutionFailed,
148    StativeProgressiveConflict,
149    UndefinedVariable {
150        name: String,
151    },
152    UseAfterMove {
153        name: String,
154    },
155    IsValueEquality {
156        variable: String,
157        value: String,
158    },
159    ZeroIndex,
160    ExpectedStatement,
161    ExpectedKeyword { keyword: String },
162    ExpectedExpression,
163    ExpectedIdentifier,
164    /// Subject and object lists have different lengths in a "respectively" construction.
165    RespectivelyLengthMismatch {
166        subject_count: usize,
167        object_count: usize,
168    },
169    /// Type mismatch during static type checking.
170    TypeMismatch {
171        expected: String,
172        found: String,
173    },
174    /// Type mismatch with context (e.g., "in argument 2 of 'compute'").
175    TypeMismatchDetailed {
176        expected: String,
177        found: String,
178        context: String,
179    },
180    /// A type variable would occur in its own definition (e.g., `T = List<T>`).
181    InfiniteType {
182        var_description: String,
183        type_description: String,
184    },
185    /// Wrong number of arguments in a function call.
186    ArityMismatch {
187        function: String,
188        expected: usize,
189        found: usize,
190    },
191    /// A field name does not exist on the struct type.
192    FieldNotFound {
193        type_name: String,
194        field_name: String,
195        available: Vec<String>,
196    },
197    /// Tried to call something that is not a function.
198    NotAFunction {
199        found_type: String,
200    },
201    /// Invalid refinement predicate in a dependent type.
202    InvalidRefinementPredicate,
203    /// Grammar error (e.g., "its" vs "it's").
204    GrammarError(String),
205    /// DRS scope violation (pronoun trapped in negation, disjunction, etc.).
206    ScopeViolation(String),
207    /// Unresolved pronoun in discourse mode - no accessible antecedent found.
208    UnresolvedPronoun {
209        gender: crate::drs::Gender,
210        number: crate::drs::Number,
211    },
212    /// The parser finished a sentence with input left over — accepting it
213    /// would silently drop the remainder's meaning.
214    TrailingTokens {
215        found: TokenType,
216    },
217    /// The program's AST nests deeper than every downstream walker
218    /// (optimizer, codegen, interpreter, VM compiler) can safely recurse —
219    /// the depth gate rejects it here so no surface ever stack-overflows.
220    AstTooDeep {
221        /// The measured (or exceeded) nesting depth.
222        depth: usize,
223        /// The enforced limit ([`crate::ast_depth::max_ast_depth`]).
224        max_depth: usize,
225    },
226    /// Custom error message (used for escape analysis, zone errors, etc.).
227    Custom(String),
228}
229
230/// A human reading of a token for error prose — never the raw `Debug` name.
231/// "a comma (',')" teaches; "Comma" is compiler debris.
232pub fn describe_token(token: &TokenType) -> String {
233    let simple = |s: &str| s.to_string();
234    match token {
235        TokenType::Period => simple("a period ('.')"),
236        TokenType::Comma => simple("a comma (',')"),
237        TokenType::Colon => simple("a colon (':')"),
238        TokenType::Newline => simple("a line break"),
239        TokenType::Indent => simple("an indent"),
240        TokenType::Dedent => simple("the end of an indented block"),
241        TokenType::EOF => simple("the end of the input"),
242        TokenType::Noun(_) => simple("a noun"),
243        TokenType::Verb { .. } => simple("a verb"),
244        TokenType::Adjective(_) | TokenType::NonIntersectiveAdjective(_) => simple("an adjective"),
245        TokenType::Adverb(_) | TokenType::ScopalAdverb(_) | TokenType::TemporalAdverb(_) => {
246            simple("an adverb")
247        }
248        TokenType::ProperName(_) => simple("a name"),
249        TokenType::Identifier => simple("an identifier"),
250        TokenType::Number(_) => simple("a number"),
251        TokenType::StringLiteral(_) | TokenType::InterpolatedString(_) => simple("a string"),
252        TokenType::CharLiteral(_) => simple("a character literal"),
253        TokenType::MoneyLiteral { .. } => simple("a money amount"),
254        TokenType::DurationLiteral { .. }
255        | TokenType::DateLiteral { .. }
256        | TokenType::TimeLiteral { .. } => simple("a time literal"),
257        TokenType::CalendarUnit(_) => simple("a calendar unit"),
258        TokenType::Pronoun { .. } => simple("a pronoun"),
259        TokenType::Article(_) => simple("an article"),
260        TokenType::Preposition(_) => simple("a preposition"),
261        TokenType::Particle(_) => simple("a particle"),
262        TokenType::Comparative(_) => simple("a comparative"),
263        TokenType::Superlative(_) => simple("a superlative"),
264        TokenType::Auxiliary(_) => simple("an auxiliary verb"),
265        TokenType::Performative(_) => simple("a performative verb"),
266        TokenType::BlockHeader { .. } => simple("a '##' block header"),
267        TokenType::EscapeBlock(_) => simple("an escape block"),
268        TokenType::Ambiguous { primary, .. } => describe_token(primary),
269        TokenType::Possessive => simple("a possessive ('s)"),
270        TokenType::LParen => simple("'('"),
271        TokenType::RParen => simple("')'"),
272        TokenType::LBracket => simple("'['"),
273        TokenType::RBracket => simple("']'"),
274        TokenType::LBrace => simple("'{'"),
275        TokenType::RBrace => simple("'}'"),
276        TokenType::Plus => simple("'+'"),
277        TokenType::Minus => simple("'-'"),
278        TokenType::Star => simple("'*'"),
279        TokenType::Slash => simple("'/'"),
280        TokenType::Percent => simple("'%'"),
281        TokenType::PlusEq => simple("'+='"),
282        TokenType::MinusEq => simple("'-='"),
283        TokenType::StarEq => simple("'*='"),
284        TokenType::SlashEq => simple("'/='"),
285        TokenType::PercentEq => simple("'%='"),
286        TokenType::StarStar => simple("'**'"),
287        TokenType::SlashSlash => simple("'//'"),
288        TokenType::Lt => simple("'<'"),
289        TokenType::Gt => simple("'>'"),
290        TokenType::LtEq => simple("'<='"),
291        TokenType::GtEq => simple("'>='"),
292        TokenType::EqEq => simple("'=='"),
293        TokenType::NotEq => simple("'!='"),
294        TokenType::Arrow => simple("'->'"),
295        TokenType::Assign => simple("'='"),
296        TokenType::Amp => simple("'&'"),
297        TokenType::VBar => simple("'|'"),
298        TokenType::Tilde => simple("'~'"),
299        TokenType::Caret => simple("'^'"),
300        TokenType::Dot => simple("'.'"),
301        other => format!("the word '{}'", format!("{other:?}").to_lowercase()),
302    }
303}
304
305/// The surface pronoun a gender/number pair names, quoted for prose.
306fn pronoun_word(gender: &crate::drs::Gender, number: &crate::drs::Number) -> &'static str {
307    use crate::drs::{Gender, Number};
308    match (number, gender) {
309        (Number::Plural, _) | (Number::Singular, Gender::Unknown) => "'they'",
310        (Number::Singular, Gender::Female) => "'she'",
311        (Number::Singular, Gender::Male) => "'he'",
312        (Number::Singular, Gender::Neuter) => "'it'",
313    }
314}
315
316#[cold]
317pub fn socratic_explanation(error: &ParseError, _interner: &Interner) -> String {
318    match &error.kind {
319        ParseErrorKind::UnexpectedToken { expected, found } => {
320            let expected = describe_token(expected);
321            format!(
322                "I was following your sentence but stumbled here: I expected {expected}, \
323                and found {} instead. The structure so far commits me to {expected} next. \
324                Is a word missing just before this, or did two thoughts run together? \
325                Splitting the sentence in two often shows which.",
326                describe_token(found)
327            )
328        }
329        ParseErrorKind::ExpectedContentWord { found } => {
330            format!(
331                "This spot needs a content word — a noun, a verb, or an adjective — but I \
332                found {} instead. Content words carry the meaning; the little words only \
333                arrange them. What thing, action, or quality did you mean to name here? \
334                Put that word in and the sentence grounds itself.",
335                describe_token(found)
336            )
337        }
338        ParseErrorKind::ExpectedCopula => "The subject here is waiting for 'is' or 'are' to \
339            link it to its predicate. English predication runs through a copula — without \
340            one, the two halves never connect. What is the subject supposed to BE? Write it \
341            as 'X is Y' ('are' for plurals)."
342            .to_string(),
343        ParseErrorKind::UnknownQuantifier { found } => {
344            format!(
345                "I needed a quantifier here — a word like 'all', 'some', or 'no' — but found \
346                {}. Quantifiers say how many things the sentence talks about, and that choice \
347                decides the whole logical shape. How many did you mean: every one, at least \
348                one, or none? Lead with the word that says so.",
349                describe_token(found)
350            )
351        }
352        ParseErrorKind::UnknownModal { found } => {
353            format!(
354                "I needed a modal here — 'must', 'can', 'may', 'should' — but found {}. \
355                Modals set the strength of the claim: necessity, possibility, permission, \
356                obligation. Which strength did you mean? Pick the modal that carries it and \
357                the logic follows.",
358                describe_token(found)
359            )
360        }
361        ParseErrorKind::ExpectedVerb { found } => {
362            format!(
363                "Every sentence needs a verb, and this one's verb should appear here — I \
364                found {} instead. The verb names the action or state everything else hangs \
365                on. What is the subject doing (or being)? Give the sentence that verb.",
366                describe_token(found)
367            )
368        }
369        ParseErrorKind::ExpectedTemporalAdverb => "I expected a time word here — 'yesterday', \
370            'tomorrow', 'always'. Temporal adverbs anchor the sentence on the timeline, and \
371            this construction promised one. When does this happen? Say it with a temporal \
372            adverb, or drop the construction that required one."
373            .to_string(),
374        ParseErrorKind::ExpectedPresuppositionTrigger => "I expected a presupposition trigger \
375            here — a word like 'stopped', 'realized', or 'regrets'. These verbs quietly \
376            assume something was already true; that hidden assumption is what this \
377            construction works with. What background fact is being taken for granted? The \
378            trigger word is what carries it."
379            .to_string(),
380        ParseErrorKind::ExpectedFocusParticle => "I expected a focus particle here — 'only', \
381            'even', 'just'. Focus particles single out one part of the sentence against its \
382            alternatives. Which word should the emphasis land on? Put the particle directly \
383            before it."
384            .to_string(),
385        ParseErrorKind::ExpectedScopalAdverb => "I expected a scopal adverb here — a word \
386            like 'necessarily' or 'possibly' that comments on the whole claim rather than \
387            the verb alone. Is the adverb meant to cover the entire sentence? If it only \
388            describes the action, it belongs next to the verb instead."
389            .to_string(),
390        ParseErrorKind::ExpectedSuperlativeAdjective => "I expected a superlative here — \
391            'tallest', 'fastest', the '-est' form. A superlative ranks one thing against all \
392            the others, and that is the comparison this sentence set up. Are you comparing \
393            against everything, or just one other thing? Use '-est' for everything, '-er \
394            than' for one."
395            .to_string(),
396        ParseErrorKind::ExpectedComparativeAdjective => "I expected a comparative here — \
397            'taller', 'faster', the '-er' form. A comparative weighs exactly two things \
398            against each other. What quality are the two compared on? Name it in its '-er' \
399            form and follow with 'than'."
400            .to_string(),
401        ParseErrorKind::ExpectedThan => "A comparative opened a comparison, and 'than' \
402            introduces the other side — but it is missing here. Taller than what, exactly? \
403            Add 'than' plus the thing being compared against."
404            .to_string(),
405        ParseErrorKind::ExpectedNumber => "This measure phrase needs a number — '2', '3.14' \
406            — and none appeared. A measure without a quantity measures nothing. How much, \
407            exactly? Put the number in front of the unit."
408            .to_string(),
409        ParseErrorKind::EmptyRestriction => "This relative clause is empty — 'the X that \
410            ...' with nothing after 'that'. A restriction narrows the noun down, so an empty \
411            one narrows nothing. Which ones did you mean? State the property that picks them \
412            out, or drop the 'that' entirely."
413            .to_string(),
414        ParseErrorKind::GappingResolutionFailed => "This reads like a gapped construction — \
415            '... and Mary, a pear' — where the verb is borrowed from the previous clause, \
416            but I found no verb there to borrow. What is the second subject doing? Either \
417            repeat the verb outright, or make sure the first clause states it plainly."
418            .to_string(),
419        ParseErrorKind::StativeProgressiveConflict => "A stative verb like 'know' or 'love' \
420            is in the progressive here, and states do not run in progress — they simply hold \
421            or they don't. Is this a state or an ongoing activity? States take the simple \
422            form ('knows'); only activities take '-ing'."
423            .to_string(),
424        ParseErrorKind::UndefinedVariable { name } => {
425            format!(
426                "I found '{name}', but nothing has declared it. Every variable starts life \
427                in a Let — reading a name before it exists gives me no value to read. Where \
428                should '{name}' get its first value? Add 'Let {name} be ...' above this \
429                line, or check the spelling against the name you declared."
430            )
431        }
432        ParseErrorKind::UseAfterMove { name } => {
433            format!(
434                "Cannot use '{name}' after giving it away. 'Give {name} to ...' transferred \
435                ownership — '{name}' belongs to the receiver now, and this later use has \
436                nothing left to hold. Who really needs to own '{name}' here? To lend it and \
437                keep it, 'Show {name}'; to hand over a duplicate, give 'a copy of {name}'."
438            )
439        }
440        ParseErrorKind::IsValueEquality { variable, value } => {
441            format!(
442                "'{variable} is {value}' reads as a type or predicate claim — 'is' asks what \
443                something IS, not which value it holds. Value comparison is spelled \
444                'equals'. Are you asking what '{variable}' is, or whether it holds {value}? \
445                For the value question, write '{variable} equals {value}'."
446            )
447        }
448        ParseErrorKind::ZeroIndex => "This asks for item 0, but LOGOS indices start at 1 — \
449            in English, 'the 1st item' is the first one; there is no item 0. Which element \
450            did you want? The first is 'item 1 of xs' — the zero-based habit is the only \
451            thing to unlearn."
452            .to_string(),
453        ParseErrorKind::ExpectedStatement => "I expected a statement here — a sentence that \
454            does something, like 'Let', 'Set', 'Show', or 'Return'. What should happen at \
455            this point in the program? Start the line with the verb that does it, and end \
456            with a period."
457            .to_string(),
458        ParseErrorKind::ExpectedKeyword { keyword } => {
459            format!(
460                "This construction needs the word '{keyword}' here to complete its shape. \
461                Statement forms have fixed connecting words — they are the joints of the \
462                sentence. Which form did you start? Insert '{keyword}' where I stopped, or \
463                compare your line against that form's example."
464            )
465        }
466        ParseErrorKind::ExpectedExpression => "I expected an expression here — a value: a \
467            number, a variable, a call, or a computation. Something just before this (a \
468            'be', a 'to', an operator) promised a value that never arrived. What value \
469            should flow into this spot? Write it, or remove the connector that promised it."
470            .to_string(),
471        ParseErrorKind::ExpectedIdentifier => "I expected a name here — an identifier for a \
472            variable, function, or field. This position says which thing the statement acts \
473            on, so a keyword or symbol cannot stand in. What is the thing called? Use its \
474            declared name, or declare it first with Let."
475            .to_string(),
476        ParseErrorKind::RespectivelyLengthMismatch { subject_count, object_count } => {
477            format!(
478                "'Respectively' pairs the two lists one-to-one, but the subject side has \
479                {subject_count} element(s) and the object side has {object_count}. With \
480                uneven lists, something ends up with no partner. Which pairing did you \
481                intend? Even the lists out, or split the sentence so each pairing is \
482                explicit."
483            )
484        }
485        ParseErrorKind::TypeMismatch { expected, found } => {
486            format!(
487                "This slot is typed '{expected}', but the value here is '{found}'. LOGOS \
488                holds every value to its declared type — that agreement is what the later \
489                guarantees stand on. Which side is right, the annotation or the value? \
490                Change the one that is lying."
491            )
492        }
493        ParseErrorKind::TypeMismatchDetailed { expected, found, context } => {
494            let ctx_note =
495                if context.is_empty() { String::new() } else { format!(" ({context})") };
496            format!(
497                "I expected '{expected}' here but found '{found}'{ctx_note}. The two sides \
498                of this position must agree on one type. Which side has it right? Adjust \
499                the annotation or the value so they tell the same story."
500            )
501        }
502        ParseErrorKind::InfiniteType { var_description, type_description } => {
503            format!(
504                "This would make an infinite type: {var_description} would have to equal \
505                {type_description}, which contains it — a type inside itself, forever. This \
506                usually means a value is being folded into its own container. Did you mean \
507                to push into the collection rather than rebuild it? A named struct or one \
508                level of indirection breaks the cycle."
509            )
510        }
511        ParseErrorKind::ArityMismatch { function, expected, found } => {
512            format!(
513                "'{function}' takes {expected} argument(s), but this call passes {found}. A \
514                call must fill every parameter — each one is a promise the function body \
515                relies on. Which parameter went missing, or which extra snuck in? Line the \
516                call up against the function's '## To' header."
517            )
518        }
519        ParseErrorKind::FieldNotFound { type_name, field_name, available } => {
520            if available.is_empty() {
521                format!(
522                    "'{type_name}' has no field named '{field_name}'. A struct's fields are \
523                    fixed at its definition — reads cannot invent new ones. Did you mean \
524                    another field, or should the '## A {type_name} has:' definition grow a \
525                    '{field_name}'?"
526                )
527            } else {
528                format!(
529                    "'{type_name}' has no field named '{field_name}'. A struct's fields are \
530                    fixed at its definition — reads cannot invent new ones. Available \
531                    fields: {}. Did you mean one of those, or should the '## A {type_name} \
532                    has:' definition grow a '{field_name}'?",
533                    available.join(", ")
534                )
535            }
536        }
537        ParseErrorKind::NotAFunction { found_type } => {
538            format!(
539                "This tries to call a value of type '{found_type}', but only functions and \
540                closures can be called. Parentheses after a name mean 'invoke this'. Did \
541                you want a function with a similar name, or is this variable shadowing the \
542                function you meant?"
543            )
544        }
545        ParseErrorKind::InvalidRefinementPredicate => "This refinement predicate is not one \
546            I can check — refinements are comparisons over the declared name, like 'x > 0' \
547            or 'n < 100'. What must always be true of this value? State it as a simple \
548            comparison; anything richer belongs in an Assert."
549            .to_string(),
550        ParseErrorKind::GrammarError(msg) => {
551            format!(
552                "Grammar: {msg}. Small grammar slips change the logical reading, so I stop \
553                rather than guess. Which meaning did you intend? Adjust the wording — the \
554                right form usually reads aloud correctly."
555            )
556        }
557        ParseErrorKind::ScopeViolation(msg) => {
558            format!(
559                "Scope problem: {msg}. A pronoun can only reach referents its clause can \
560                see — things introduced under a negation or inside an 'or' are walled off \
561                from later sentences. Who is the pronoun meant to point at? Name the \
562                referent outright, or introduce it outside the wall."
563            )
564        }
565        ParseErrorKind::UnresolvedPronoun { gender, number } => {
566            let word = pronoun_word(gender, number);
567            format!(
568                "This pronoun ({word}) has nothing to refer to — no earlier sentence \
569                introduced a matching referent this clause can reach. Pronouns only look \
570                backward through accessible discourse. Who is {word} here? Introduce that \
571                referent in an earlier sentence, or use the name directly."
572            )
573        }
574        ParseErrorKind::TrailingTokens { found } => {
575            format!(
576                "I understood the sentence up to here, but the rest — beginning with {} — \
577                does not fit the structure I built. Accepting it would silently drop that \
578                meaning, and LOGOS never drops meaning. Is a connective missing, or are two \
579                sentences sharing one period? End the first thought with '.' and start \
580                fresh.",
581                describe_token(found)
582            )
583        }
584        ParseErrorKind::AstTooDeep { depth, max_depth } => {
585            format!(
586                "This program nests expressions or blocks {depth} levels deep, past the \
587                current {max_depth}-level limit — a tower this tall usually comes from \
588                generated code, and every downstream walker would overflow on it. Could \
589                each layer land in its own 'Let'? Intermediate bindings reset the depth to \
590                one; or raise the gate with LOGOS_MAX_AST_DEPTH={suggested} if your stacks \
591                are deep.",
592                suggested = (depth + depth / 4).next_power_of_two()
593            )
594        }
595        ParseErrorKind::Custom(msg) => msg.clone(),
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use crate::token::Span;
603
604    #[test]
605    fn parse_error_has_span() {
606        let error = ParseError {
607            kind: ParseErrorKind::ExpectedCopula,
608            span: Span::new(5, 10),
609        };
610        assert_eq!(error.span.start, 5);
611        assert_eq!(error.span.end, 10);
612    }
613
614    #[test]
615    fn display_with_source_shows_line_and_underline() {
616        let error = ParseError {
617            kind: ParseErrorKind::ExpectedCopula,
618            span: Span::new(8, 14),
619        };
620        let source = "All men mortal are.";
621        let display = error.display_with_source(source);
622        assert!(display.contains("mortal"), "Should contain source word: {}", display);
623        assert!(display.contains("^^^^^^"), "Should contain underline: {}", display);
624    }
625
626    #[test]
627    fn display_with_source_suggests_typo_fix() {
628        let error = ParseError {
629            kind: ParseErrorKind::ExpectedCopula,
630            span: Span::new(0, 5),
631        };
632        let source = "logoc is the study of reason.";
633        let display = error.display_with_source(source);
634        assert!(display.contains("did you mean"), "Should suggest fix: {}", display);
635        assert!(display.contains("logic"), "Should suggest 'logic': {}", display);
636    }
637
638    #[test]
639    fn display_with_source_has_color_codes() {
640        let error = ParseError {
641            kind: ParseErrorKind::ExpectedCopula,
642            span: Span::new(0, 3),
643        };
644        let source = "Alll men are mortal.";
645        let display = error.display_with_source(source);
646        assert!(display.contains("\x1b["), "Should contain ANSI escape codes: {}", display);
647    }
648
649    // =========================================================================
650    // Phase 4 — Type error reporting variants
651    // =========================================================================
652
653    #[test]
654    fn type_mismatch_detailed_socratic_mentions_types() {
655        let interner = logicaffeine_base::Interner::new();
656        let error = ParseError {
657            kind: ParseErrorKind::TypeMismatchDetailed {
658                expected: "Int".to_string(),
659                found: "Bool".to_string(),
660                context: "in let binding".to_string(),
661            },
662            span: Span::new(0, 0),
663        };
664        let explanation = socratic_explanation(&error, &interner);
665        assert!(explanation.contains("Int"), "Should mention expected type: {}", explanation);
666        assert!(explanation.contains("Bool"), "Should mention found type: {}", explanation);
667        assert!(explanation.contains("let binding"), "Should include context: {}", explanation);
668    }
669
670    #[test]
671    fn type_mismatch_detailed_without_context_is_clean() {
672        let interner = logicaffeine_base::Interner::new();
673        let error = ParseError {
674            kind: ParseErrorKind::TypeMismatchDetailed {
675                expected: "Text".to_string(),
676                found: "Int".to_string(),
677                context: String::new(),
678            },
679            span: Span::new(0, 0),
680        };
681        let explanation = socratic_explanation(&error, &interner);
682        assert!(explanation.contains("Text"), "Should mention expected type: {}", explanation);
683        assert!(explanation.contains("Int"), "Should mention found type: {}", explanation);
684        // No spurious "()" from empty context
685        assert!(!explanation.contains("()"), "Empty context should not leave '()': {}", explanation);
686    }
687
688    #[test]
689    fn infinite_type_socratic_mentions_both_descriptions() {
690        let interner = logicaffeine_base::Interner::new();
691        let error = ParseError {
692            kind: ParseErrorKind::InfiniteType {
693                var_description: "type variable α0".to_string(),
694                type_description: "Seq of α0".to_string(),
695            },
696            span: Span::new(0, 0),
697        };
698        let explanation = socratic_explanation(&error, &interner);
699        assert!(explanation.contains("α0"), "Should mention var: {}", explanation);
700        assert!(explanation.contains("Seq of α0"), "Should mention type: {}", explanation);
701    }
702
703    #[test]
704    fn arity_mismatch_socratic_mentions_function_and_counts() {
705        let interner = logicaffeine_base::Interner::new();
706        let error = ParseError {
707            kind: ParseErrorKind::ArityMismatch {
708                function: "double".to_string(),
709                expected: 1,
710                found: 3,
711            },
712            span: Span::new(0, 0),
713        };
714        let explanation = socratic_explanation(&error, &interner);
715        assert!(explanation.contains("double"), "Should name the function: {}", explanation);
716        assert!(explanation.contains("1"), "Should mention expected count: {}", explanation);
717        assert!(explanation.contains("3"), "Should mention found count: {}", explanation);
718    }
719
720    #[test]
721    fn field_not_found_socratic_mentions_type_and_field() {
722        let interner = logicaffeine_base::Interner::new();
723        let error = ParseError {
724            kind: ParseErrorKind::FieldNotFound {
725                type_name: "Point".to_string(),
726                field_name: "z".to_string(),
727                available: vec!["x".to_string(), "y".to_string()],
728            },
729            span: Span::new(0, 0),
730        };
731        let explanation = socratic_explanation(&error, &interner);
732        assert!(explanation.contains("Point"), "Should name the type: {}", explanation);
733        assert!(explanation.contains("z"), "Should name the missing field: {}", explanation);
734        assert!(explanation.contains("x"), "Should list available fields: {}", explanation);
735        assert!(explanation.contains("y"), "Should list available fields: {}", explanation);
736    }
737
738    #[test]
739    fn not_a_function_socratic_mentions_found_type() {
740        let interner = logicaffeine_base::Interner::new();
741        let error = ParseError {
742            kind: ParseErrorKind::NotAFunction {
743                found_type: "Int".to_string(),
744            },
745            span: Span::new(0, 0),
746        };
747        let explanation = socratic_explanation(&error, &interner);
748        assert!(explanation.contains("Int"), "Should mention the type found: {}", explanation);
749        assert!(explanation.to_lowercase().contains("function"), "Should mention function: {}", explanation);
750    }
751}