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 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 RespectivelyLengthMismatch {
136 subject_count: usize,
137 object_count: usize,
138 },
139 TypeMismatch {
141 expected: String,
142 found: String,
143 },
144 TypeMismatchDetailed {
146 expected: String,
147 found: String,
148 context: String,
149 },
150 InfiniteType {
152 var_description: String,
153 type_description: String,
154 },
155 ArityMismatch {
157 function: String,
158 expected: usize,
159 found: usize,
160 },
161 FieldNotFound {
163 type_name: String,
164 field_name: String,
165 available: Vec<String>,
166 },
167 NotAFunction {
169 found_type: String,
170 },
171 InvalidRefinementPredicate,
173 GrammarError(String),
175 ScopeViolation(String),
177 UnresolvedPronoun {
179 gender: crate::drs::Gender,
180 number: crate::drs::Number,
181 },
182 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 #[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 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}