1use logicaffeine_base::Interner;
17use crate::style::Style;
18use crate::suggest::{find_similar, KNOWN_WORDS};
19use crate::token::{Span, TokenType};
20
21#[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 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 RespectivelyLengthMismatch {
166 subject_count: usize,
167 object_count: usize,
168 },
169 TypeMismatch {
171 expected: String,
172 found: String,
173 },
174 TypeMismatchDetailed {
176 expected: String,
177 found: String,
178 context: String,
179 },
180 InfiniteType {
182 var_description: String,
183 type_description: String,
184 },
185 ArityMismatch {
187 function: String,
188 expected: usize,
189 found: usize,
190 },
191 FieldNotFound {
193 type_name: String,
194 field_name: String,
195 available: Vec<String>,
196 },
197 NotAFunction {
199 found_type: String,
200 },
201 InvalidRefinementPredicate,
203 GrammarError(String),
205 ScopeViolation(String),
207 UnresolvedPronoun {
209 gender: crate::drs::Gender,
210 number: crate::drs::Number,
211 },
212 TrailingTokens {
215 found: TokenType,
216 },
217 AstTooDeep {
221 depth: usize,
223 max_depth: usize,
225 },
226 Custom(String),
228}
229
230pub 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
305fn 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 #[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 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}