1use serde::{Deserialize, Serialize};
15
16use logicaffeine_base::Interner;
17
18use crate::error::{
19 describe_token, pronoun_word, socratic_explanation, ParseError, ParseErrorKind,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum DiagSeverity {
25 Error,
26 Warning,
27 Info,
28}
29
30impl DiagSeverity {
31 pub fn as_str(self) -> &'static str {
33 match self {
34 DiagSeverity::Error => "error",
35 DiagSeverity::Warning => "warning",
36 DiagSeverity::Info => "info",
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct DiagSpan {
44 pub start: usize,
45 pub end: usize,
46 pub start_line: usize,
47 pub start_col: usize,
48 pub end_line: usize,
49 pub end_col: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct RelatedInfo {
55 pub file: Option<String>,
56 pub span: DiagSpan,
57 pub message: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct Diagnostic {
63 pub code: String,
65 pub severity: String,
67 pub file: Option<String>,
69 pub span: DiagSpan,
70 pub message: String,
72 pub explanation: String,
74 pub suggestion: Option<String>,
76 pub quickfix: Option<String>,
78 pub docs: Option<String>,
80 pub related: Vec<RelatedInfo>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
86pub struct CatalogEntry {
87 pub variant: &'static str,
89 pub code: &'static str,
90 pub severity: &'static str,
91 pub headline: String,
92 pub explanation: String,
93 pub quickfix: Option<&'static str>,
94 pub docs: Option<String>,
95}
96
97impl ParseErrorKind {
98 pub fn code(&self) -> &'static str {
104 match self {
105 ParseErrorKind::UnexpectedToken { .. } => "unexpected-token",
106 ParseErrorKind::ExpectedContentWord { .. } => "expected-content-word",
107 ParseErrorKind::ExpectedCopula => "expected-copula",
108 ParseErrorKind::UnknownQuantifier { .. } => "unknown-quantifier",
109 ParseErrorKind::UnknownModal { .. } => "unknown-modal",
110 ParseErrorKind::ExpectedVerb { .. } => "expected-verb",
111 ParseErrorKind::ExpectedTemporalAdverb => "expected-temporal-adverb",
112 ParseErrorKind::ExpectedPresuppositionTrigger => "expected-presupposition-trigger",
113 ParseErrorKind::ExpectedFocusParticle => "expected-focus-particle",
114 ParseErrorKind::ExpectedScopalAdverb => "expected-scopal-adverb",
115 ParseErrorKind::ExpectedSuperlativeAdjective => "expected-superlative-adjective",
116 ParseErrorKind::ExpectedComparativeAdjective => "expected-comparative-adjective",
117 ParseErrorKind::ExpectedThan => "expected-than",
118 ParseErrorKind::ExpectedNumber => "expected-number",
119 ParseErrorKind::EmptyRestriction => "empty-restriction",
120 ParseErrorKind::GappingResolutionFailed => "gapping-resolution-failed",
121 ParseErrorKind::StativeProgressiveConflict => "stative-progressive-conflict",
122 ParseErrorKind::UndefinedVariable { .. } => "undefined-variable",
123 ParseErrorKind::UseAfterMove { .. } => "use-after-move",
124 ParseErrorKind::IsValueEquality { .. } => "is-value-equality",
125 ParseErrorKind::ZeroIndex => "zero-index",
126 ParseErrorKind::ExpectedStatement => "expected-statement",
127 ParseErrorKind::ExpectedKeyword { .. } => "expected-keyword",
128 ParseErrorKind::ExpectedExpression => "expected-expression",
129 ParseErrorKind::ExpectedIdentifier => "expected-identifier",
130 ParseErrorKind::RespectivelyLengthMismatch { .. } => "respectively-length-mismatch",
131 ParseErrorKind::TypeMismatch { .. } | ParseErrorKind::TypeMismatchDetailed { .. } => {
132 "type-mismatch"
133 }
134 ParseErrorKind::InfiniteType { .. } => "infinite-type",
135 ParseErrorKind::ArityMismatch { .. } => "arity-mismatch",
136 ParseErrorKind::FieldNotFound { .. } => "field-not-found",
137 ParseErrorKind::NotAFunction { .. } => "not-a-function",
138 ParseErrorKind::InvalidRefinementPredicate => "invalid-refinement-predicate",
139 ParseErrorKind::GrammarError(_) => "grammar-error",
140 ParseErrorKind::ScopeViolation(_) => "scope-violation",
141 ParseErrorKind::UnresolvedPronoun { .. } => "unresolved-pronoun",
142 ParseErrorKind::TrailingTokens { .. } => "trailing-tokens",
143 ParseErrorKind::AstTooDeep { .. } => "ast-too-deep",
144 ParseErrorKind::Custom(_) => "custom",
145 }
146 }
147
148 pub fn severity(&self) -> DiagSeverity {
150 match self {
151 ParseErrorKind::IsValueEquality { .. } | ParseErrorKind::GrammarError(_) => {
153 DiagSeverity::Warning
154 }
155 ParseErrorKind::ZeroIndex => DiagSeverity::Info,
157 ParseErrorKind::UnexpectedToken { .. }
158 | ParseErrorKind::ExpectedContentWord { .. }
159 | ParseErrorKind::ExpectedCopula
160 | ParseErrorKind::UnknownQuantifier { .. }
161 | ParseErrorKind::UnknownModal { .. }
162 | ParseErrorKind::ExpectedVerb { .. }
163 | ParseErrorKind::ExpectedTemporalAdverb
164 | ParseErrorKind::ExpectedPresuppositionTrigger
165 | ParseErrorKind::ExpectedFocusParticle
166 | ParseErrorKind::ExpectedScopalAdverb
167 | ParseErrorKind::ExpectedSuperlativeAdjective
168 | ParseErrorKind::ExpectedComparativeAdjective
169 | ParseErrorKind::ExpectedThan
170 | ParseErrorKind::ExpectedNumber
171 | ParseErrorKind::EmptyRestriction
172 | ParseErrorKind::GappingResolutionFailed
173 | ParseErrorKind::StativeProgressiveConflict
174 | ParseErrorKind::UndefinedVariable { .. }
175 | ParseErrorKind::UseAfterMove { .. }
176 | ParseErrorKind::ExpectedStatement
177 | ParseErrorKind::ExpectedKeyword { .. }
178 | ParseErrorKind::ExpectedExpression
179 | ParseErrorKind::ExpectedIdentifier
180 | ParseErrorKind::RespectivelyLengthMismatch { .. }
181 | ParseErrorKind::TypeMismatch { .. }
182 | ParseErrorKind::TypeMismatchDetailed { .. }
183 | ParseErrorKind::InfiniteType { .. }
184 | ParseErrorKind::ArityMismatch { .. }
185 | ParseErrorKind::FieldNotFound { .. }
186 | ParseErrorKind::NotAFunction { .. }
187 | ParseErrorKind::InvalidRefinementPredicate
188 | ParseErrorKind::ScopeViolation(_)
189 | ParseErrorKind::UnresolvedPronoun { .. }
190 | ParseErrorKind::TrailingTokens { .. }
191 | ParseErrorKind::AstTooDeep { .. }
192 | ParseErrorKind::Custom(_) => DiagSeverity::Error,
193 }
194 }
195
196 pub fn headline(&self) -> String {
198 match self {
199 ParseErrorKind::UnexpectedToken { expected, found } => format!(
200 "expected {}, found {}",
201 describe_token(expected),
202 describe_token(found)
203 ),
204 ParseErrorKind::ExpectedContentWord { found } => {
205 format!("expected a content word, found {}", describe_token(found))
206 }
207 ParseErrorKind::ExpectedCopula => "expected 'is' or 'are'".to_string(),
208 ParseErrorKind::UnknownQuantifier { found } => {
209 format!("expected a quantifier, found {}", describe_token(found))
210 }
211 ParseErrorKind::UnknownModal { found } => {
212 format!("expected a modal, found {}", describe_token(found))
213 }
214 ParseErrorKind::ExpectedVerb { found } => {
215 format!("expected a verb, found {}", describe_token(found))
216 }
217 ParseErrorKind::ExpectedTemporalAdverb => "expected a temporal adverb".to_string(),
218 ParseErrorKind::ExpectedPresuppositionTrigger => {
219 "expected a presupposition trigger".to_string()
220 }
221 ParseErrorKind::ExpectedFocusParticle => "expected a focus particle".to_string(),
222 ParseErrorKind::ExpectedScopalAdverb => "expected a scopal adverb".to_string(),
223 ParseErrorKind::ExpectedSuperlativeAdjective => {
224 "expected a superlative adjective".to_string()
225 }
226 ParseErrorKind::ExpectedComparativeAdjective => {
227 "expected a comparative adjective".to_string()
228 }
229 ParseErrorKind::ExpectedThan => "expected 'than' to complete the comparison".to_string(),
230 ParseErrorKind::ExpectedNumber => "expected a number".to_string(),
231 ParseErrorKind::EmptyRestriction => "empty relative clause".to_string(),
232 ParseErrorKind::GappingResolutionFailed => {
233 "no verb to borrow for the gapped clause".to_string()
234 }
235 ParseErrorKind::StativeProgressiveConflict => {
236 "stative verb used in the progressive".to_string()
237 }
238 ParseErrorKind::UndefinedVariable { name } => format!("'{name}' is not declared"),
239 ParseErrorKind::UseAfterMove { name } => {
240 format!("cannot use '{name}' after giving it away")
241 }
242 ParseErrorKind::IsValueEquality { variable, value } => {
243 format!("'{variable} is {value}' compares values; use 'equals'")
244 }
245 ParseErrorKind::ZeroIndex => "indices start at 1; there is no item 0".to_string(),
246 ParseErrorKind::ExpectedStatement => "expected a statement".to_string(),
247 ParseErrorKind::ExpectedKeyword { keyword } => {
248 format!("expected the word '{keyword}'")
249 }
250 ParseErrorKind::ExpectedExpression => "expected an expression".to_string(),
251 ParseErrorKind::ExpectedIdentifier => "expected a name".to_string(),
252 ParseErrorKind::RespectivelyLengthMismatch { subject_count, object_count } => format!(
253 "'respectively' lists differ: {subject_count} subject(s) vs {object_count} object(s)"
254 ),
255 ParseErrorKind::TypeMismatch { expected, found } => {
256 format!("expected '{expected}', found '{found}'")
257 }
258 ParseErrorKind::TypeMismatchDetailed { expected, found, context } => {
259 if context.is_empty() {
260 format!("expected '{expected}', found '{found}'")
261 } else {
262 format!("expected '{expected}', found '{found}' ({context})")
263 }
264 }
265 ParseErrorKind::InfiniteType { var_description, .. } => {
266 format!("infinite type: {var_description} would contain itself")
267 }
268 ParseErrorKind::ArityMismatch { function, expected, found } => {
269 format!("'{function}' takes {expected} argument(s), but this call passes {found}")
270 }
271 ParseErrorKind::FieldNotFound { type_name, field_name, .. } => {
272 format!("'{type_name}' has no field named '{field_name}'")
273 }
274 ParseErrorKind::NotAFunction { found_type } => {
275 format!("cannot call a value of type '{found_type}'")
276 }
277 ParseErrorKind::InvalidRefinementPredicate => {
278 "refinement predicate is not checkable".to_string()
279 }
280 ParseErrorKind::GrammarError(msg) => format!("grammar: {msg}"),
281 ParseErrorKind::ScopeViolation(msg) => format!("scope: {msg}"),
282 ParseErrorKind::UnresolvedPronoun { gender, number } => {
283 format!("pronoun {} has no antecedent", pronoun_word(gender, number))
284 }
285 ParseErrorKind::TrailingTokens { found } => {
286 format!("input left over, beginning with {}", describe_token(found))
287 }
288 ParseErrorKind::AstTooDeep { depth, max_depth } => {
289 format!("AST nests {depth} levels deep; the limit is {max_depth}")
290 }
291 ParseErrorKind::Custom(msg) => msg.clone(),
292 }
293 }
294
295 pub fn variant_name(&self) -> &'static str {
297 match self {
298 ParseErrorKind::UnexpectedToken { .. } => "UnexpectedToken",
299 ParseErrorKind::ExpectedContentWord { .. } => "ExpectedContentWord",
300 ParseErrorKind::ExpectedCopula => "ExpectedCopula",
301 ParseErrorKind::UnknownQuantifier { .. } => "UnknownQuantifier",
302 ParseErrorKind::UnknownModal { .. } => "UnknownModal",
303 ParseErrorKind::ExpectedVerb { .. } => "ExpectedVerb",
304 ParseErrorKind::ExpectedTemporalAdverb => "ExpectedTemporalAdverb",
305 ParseErrorKind::ExpectedPresuppositionTrigger => "ExpectedPresuppositionTrigger",
306 ParseErrorKind::ExpectedFocusParticle => "ExpectedFocusParticle",
307 ParseErrorKind::ExpectedScopalAdverb => "ExpectedScopalAdverb",
308 ParseErrorKind::ExpectedSuperlativeAdjective => "ExpectedSuperlativeAdjective",
309 ParseErrorKind::ExpectedComparativeAdjective => "ExpectedComparativeAdjective",
310 ParseErrorKind::ExpectedThan => "ExpectedThan",
311 ParseErrorKind::ExpectedNumber => "ExpectedNumber",
312 ParseErrorKind::EmptyRestriction => "EmptyRestriction",
313 ParseErrorKind::GappingResolutionFailed => "GappingResolutionFailed",
314 ParseErrorKind::StativeProgressiveConflict => "StativeProgressiveConflict",
315 ParseErrorKind::UndefinedVariable { .. } => "UndefinedVariable",
316 ParseErrorKind::UseAfterMove { .. } => "UseAfterMove",
317 ParseErrorKind::IsValueEquality { .. } => "IsValueEquality",
318 ParseErrorKind::ZeroIndex => "ZeroIndex",
319 ParseErrorKind::ExpectedStatement => "ExpectedStatement",
320 ParseErrorKind::ExpectedKeyword { .. } => "ExpectedKeyword",
321 ParseErrorKind::ExpectedExpression => "ExpectedExpression",
322 ParseErrorKind::ExpectedIdentifier => "ExpectedIdentifier",
323 ParseErrorKind::RespectivelyLengthMismatch { .. } => "RespectivelyLengthMismatch",
324 ParseErrorKind::TypeMismatch { .. } => "TypeMismatch",
325 ParseErrorKind::TypeMismatchDetailed { .. } => "TypeMismatchDetailed",
326 ParseErrorKind::InfiniteType { .. } => "InfiniteType",
327 ParseErrorKind::ArityMismatch { .. } => "ArityMismatch",
328 ParseErrorKind::FieldNotFound { .. } => "FieldNotFound",
329 ParseErrorKind::NotAFunction { .. } => "NotAFunction",
330 ParseErrorKind::InvalidRefinementPredicate => "InvalidRefinementPredicate",
331 ParseErrorKind::GrammarError(_) => "GrammarError",
332 ParseErrorKind::ScopeViolation(_) => "ScopeViolation",
333 ParseErrorKind::UnresolvedPronoun { .. } => "UnresolvedPronoun",
334 ParseErrorKind::TrailingTokens { .. } => "TrailingTokens",
335 ParseErrorKind::AstTooDeep { .. } => "AstTooDeep",
336 ParseErrorKind::Custom(_) => "Custom",
337 }
338 }
339
340 pub fn quickfix_note(&self) -> Option<&'static str> {
342 match self {
343 ParseErrorKind::IsValueEquality { .. } => Some("Use 'equals' for value comparison"),
344 ParseErrorKind::ZeroIndex => Some("Use 1-based indexing"),
345 ParseErrorKind::UseAfterMove { .. } => Some("Use 'a copy of …' instead"),
346 ParseErrorKind::UndefinedVariable { .. } => {
347 Some("Did you mean '<nearest definition>'?")
348 }
349 ParseErrorKind::UnexpectedToken { .. }
350 | ParseErrorKind::ExpectedContentWord { .. }
351 | ParseErrorKind::ExpectedCopula
352 | ParseErrorKind::UnknownQuantifier { .. }
353 | ParseErrorKind::UnknownModal { .. }
354 | ParseErrorKind::ExpectedVerb { .. }
355 | ParseErrorKind::ExpectedTemporalAdverb
356 | ParseErrorKind::ExpectedPresuppositionTrigger
357 | ParseErrorKind::ExpectedFocusParticle
358 | ParseErrorKind::ExpectedScopalAdverb
359 | ParseErrorKind::ExpectedSuperlativeAdjective
360 | ParseErrorKind::ExpectedComparativeAdjective
361 | ParseErrorKind::ExpectedThan
362 | ParseErrorKind::ExpectedNumber
363 | ParseErrorKind::EmptyRestriction
364 | ParseErrorKind::GappingResolutionFailed
365 | ParseErrorKind::StativeProgressiveConflict
366 | ParseErrorKind::ExpectedStatement
367 | ParseErrorKind::ExpectedKeyword { .. }
368 | ParseErrorKind::ExpectedExpression
369 | ParseErrorKind::ExpectedIdentifier
370 | ParseErrorKind::RespectivelyLengthMismatch { .. }
371 | ParseErrorKind::TypeMismatch { .. }
372 | ParseErrorKind::TypeMismatchDetailed { .. }
373 | ParseErrorKind::InfiniteType { .. }
374 | ParseErrorKind::ArityMismatch { .. }
375 | ParseErrorKind::FieldNotFound { .. }
376 | ParseErrorKind::NotAFunction { .. }
377 | ParseErrorKind::InvalidRefinementPredicate
378 | ParseErrorKind::GrammarError(_)
379 | ParseErrorKind::ScopeViolation(_)
380 | ParseErrorKind::UnresolvedPronoun { .. }
381 | ParseErrorKind::TrailingTokens { .. }
382 | ParseErrorKind::AstTooDeep { .. }
383 | ParseErrorKind::Custom(_) => None,
384 }
385 }
386
387 pub fn docs_anchor(&self) -> Option<&'static str> {
389 match self {
390 ParseErrorKind::IsValueEquality { .. } => Some("3-arithmetic-comparison-logic-bitwise"),
391 ParseErrorKind::ZeroIndex => Some("5-collections"),
392 ParseErrorKind::UseAfterMove { .. } => Some("13-output"),
393 ParseErrorKind::UndefinedVariable { .. } => Some("2-variables--mutation"),
394 ParseErrorKind::TypeMismatch { .. } | ParseErrorKind::TypeMismatchDetailed { .. } => {
395 Some("2-variables--mutation")
396 }
397 ParseErrorKind::ArityMismatch { .. } | ParseErrorKind::NotAFunction { .. } => {
398 Some("7-functions--closures")
399 }
400 ParseErrorKind::FieldNotFound { .. } => Some("8-structs-enums--field-access"),
401 ParseErrorKind::UnexpectedToken { .. }
402 | ParseErrorKind::ExpectedContentWord { .. }
403 | ParseErrorKind::ExpectedCopula
404 | ParseErrorKind::UnknownQuantifier { .. }
405 | ParseErrorKind::UnknownModal { .. }
406 | ParseErrorKind::ExpectedVerb { .. }
407 | ParseErrorKind::ExpectedTemporalAdverb
408 | ParseErrorKind::ExpectedPresuppositionTrigger
409 | ParseErrorKind::ExpectedFocusParticle
410 | ParseErrorKind::ExpectedScopalAdverb
411 | ParseErrorKind::ExpectedSuperlativeAdjective
412 | ParseErrorKind::ExpectedComparativeAdjective
413 | ParseErrorKind::ExpectedThan
414 | ParseErrorKind::ExpectedNumber
415 | ParseErrorKind::EmptyRestriction
416 | ParseErrorKind::GappingResolutionFailed
417 | ParseErrorKind::StativeProgressiveConflict
418 | ParseErrorKind::ExpectedStatement
419 | ParseErrorKind::ExpectedKeyword { .. }
420 | ParseErrorKind::ExpectedExpression
421 | ParseErrorKind::ExpectedIdentifier
422 | ParseErrorKind::RespectivelyLengthMismatch { .. }
423 | ParseErrorKind::InfiniteType { .. }
424 | ParseErrorKind::InvalidRefinementPredicate
425 | ParseErrorKind::GrammarError(_)
426 | ParseErrorKind::ScopeViolation(_)
427 | ParseErrorKind::UnresolvedPronoun { .. }
428 | ParseErrorKind::TrailingTokens { .. }
429 | ParseErrorKind::AstTooDeep { .. }
430 | ParseErrorKind::Custom(_) => None,
431 }
432 }
433}
434
435pub fn guide_url(anchor: &str) -> String {
438 format!(
439 "https://github.com/Brahmastra-Labs/logicaffeine/blob/main/LOGOS_QUICKGUIDE.md#{anchor}"
440 )
441}
442
443pub fn line_col(source: &str, byte: usize) -> (usize, usize) {
445 let mut line = 1usize;
446 let mut col = 1usize;
447 for (i, c) in source.char_indices() {
448 if i >= byte {
449 break;
450 }
451 if c == '\n' {
452 line += 1;
453 col = 1;
454 } else {
455 col += 1;
456 }
457 }
458 (line, col)
459}
460
461pub fn diagnostic_for(
463 error: &ParseError,
464 source: &str,
465 file: Option<&str>,
466 interner: &Interner,
467) -> Diagnostic {
468 let (start_line, start_col) = line_col(source, error.span.start);
469 let (end_line, end_col) = line_col(source, error.span.end);
470 Diagnostic {
471 code: error.kind.code().to_string(),
472 severity: error.kind.severity().as_str().to_string(),
473 file: file.map(str::to_string),
474 span: DiagSpan {
475 start: error.span.start,
476 end: error.span.end,
477 start_line,
478 start_col,
479 end_line,
480 end_col,
481 },
482 message: error.kind.headline(),
483 explanation: socratic_explanation(error, interner),
484 suggestion: error
485 .suggestion(source)
486 .map(|word| format!("did you mean '{word}'?")),
487 quickfix: error.kind.quickfix_note().map(str::to_string),
488 docs: error.kind.docs_anchor().map(guide_url),
489 related: Vec::new(),
490 }
491}
492
493pub fn sample_kinds() -> Vec<(&'static str, ParseErrorKind)> {
498 use crate::drs::{Gender, Number};
499 use crate::token::TokenType;
500
501 let kinds = vec![
502 ParseErrorKind::UnexpectedToken {
503 expected: TokenType::Period,
504 found: TokenType::Comma,
505 },
506 ParseErrorKind::ExpectedContentWord {
507 found: TokenType::Comma,
508 },
509 ParseErrorKind::ExpectedCopula,
510 ParseErrorKind::UnknownQuantifier {
511 found: TokenType::Comma,
512 },
513 ParseErrorKind::UnknownModal {
514 found: TokenType::Comma,
515 },
516 ParseErrorKind::ExpectedVerb {
517 found: TokenType::Comma,
518 },
519 ParseErrorKind::ExpectedTemporalAdverb,
520 ParseErrorKind::ExpectedPresuppositionTrigger,
521 ParseErrorKind::ExpectedFocusParticle,
522 ParseErrorKind::ExpectedScopalAdverb,
523 ParseErrorKind::ExpectedSuperlativeAdjective,
524 ParseErrorKind::ExpectedComparativeAdjective,
525 ParseErrorKind::ExpectedThan,
526 ParseErrorKind::ExpectedNumber,
527 ParseErrorKind::EmptyRestriction,
528 ParseErrorKind::GappingResolutionFailed,
529 ParseErrorKind::StativeProgressiveConflict,
530 ParseErrorKind::UndefinedVariable {
531 name: "x".to_string(),
532 },
533 ParseErrorKind::UseAfterMove {
534 name: "x".to_string(),
535 },
536 ParseErrorKind::IsValueEquality {
537 variable: "x".to_string(),
538 value: "5".to_string(),
539 },
540 ParseErrorKind::ZeroIndex,
541 ParseErrorKind::ExpectedStatement,
542 ParseErrorKind::ExpectedKeyword {
543 keyword: "to".to_string(),
544 },
545 ParseErrorKind::ExpectedExpression,
546 ParseErrorKind::ExpectedIdentifier,
547 ParseErrorKind::RespectivelyLengthMismatch {
548 subject_count: 2,
549 object_count: 3,
550 },
551 ParseErrorKind::TypeMismatch {
552 expected: "Int".to_string(),
553 found: "Text".to_string(),
554 },
555 ParseErrorKind::TypeMismatchDetailed {
556 expected: "Int".to_string(),
557 found: "Text".to_string(),
558 context: "in argument 2 of 'compute'".to_string(),
559 },
560 ParseErrorKind::InfiniteType {
561 var_description: "T".to_string(),
562 type_description: "Seq of T".to_string(),
563 },
564 ParseErrorKind::ArityMismatch {
565 function: "greet".to_string(),
566 expected: 2,
567 found: 1,
568 },
569 ParseErrorKind::FieldNotFound {
570 type_name: "Point".to_string(),
571 field_name: "z".to_string(),
572 available: vec!["x".to_string(), "y".to_string()],
573 },
574 ParseErrorKind::NotAFunction {
575 found_type: "Int".to_string(),
576 },
577 ParseErrorKind::InvalidRefinementPredicate,
578 ParseErrorKind::GrammarError("its vs it's".to_string()),
579 ParseErrorKind::ScopeViolation("pronoun trapped in negation".to_string()),
580 ParseErrorKind::UnresolvedPronoun {
581 gender: Gender::Female,
582 number: Number::Singular,
583 },
584 ParseErrorKind::TrailingTokens {
585 found: TokenType::Comma,
586 },
587 ParseErrorKind::AstTooDeep {
588 depth: 3000,
589 max_depth: 2048,
590 },
591 ParseErrorKind::Custom("zone escape".to_string()),
592 ];
593
594 kinds
595 .into_iter()
596 .map(|kind| (kind.variant_name(), kind))
597 .collect()
598}
599
600pub fn catalog() -> Vec<CatalogEntry> {
602 let interner = Interner::new();
603 sample_kinds()
604 .into_iter()
605 .map(|(variant, kind)| {
606 let error = ParseError {
607 kind,
608 span: crate::token::Span::new(0, 0),
609 };
610 CatalogEntry {
611 variant,
612 code: error.kind.code(),
613 severity: error.kind.severity().as_str(),
614 headline: error.kind.headline(),
615 explanation: socratic_explanation(&error, &interner),
616 quickfix: error.kind.quickfix_note(),
617 docs: error.kind.docs_anchor().map(guide_url),
618 }
619 })
620 .collect()
621}
622
623fn catalog_by_code(entries: &[CatalogEntry]) -> Vec<(&'static str, Vec<&CatalogEntry>)> {
625 let mut order: Vec<&'static str> = Vec::new();
626 let mut groups: std::collections::BTreeMap<&'static str, Vec<&CatalogEntry>> =
627 std::collections::BTreeMap::new();
628 for entry in entries {
629 if !groups.contains_key(entry.code) {
630 order.push(entry.code);
631 }
632 groups.entry(entry.code).or_default().push(entry);
633 }
634 order
635 .into_iter()
636 .map(|code| (code, groups[code].clone()))
637 .collect()
638}
639
640pub fn catalog_markdown() -> String {
643 let entries = catalog();
644 let mut out = String::new();
645 out.push_str("# LOGOS Error Catalog\n\n");
646 out.push_str(
647 "Every diagnostic the LOGOS toolchain emits, keyed by its stable\n\
648 `logos-diagnostic/1` code — the `code` field of `largo check --json`,\n\
649 `largo run --json`, and the MCP `logos_check` tool. Look a code up here\n\
650 or with `largo explain <code>`.\n\n\
651 Generated from `logicaffeine_language::diagnostic::catalog()`. Do not\n\
652 edit by hand; run `./scripts/release/gen-error-catalog.sh` after changing\n\
653 `ParseErrorKind`.\n",
654 );
655 for (code, group) in catalog_by_code(&entries) {
656 out.push_str(&format!("\n## `{code}`\n\n"));
657 let severity = group[0].severity;
658 out.push_str(&format!("- **Severity**: {severity}\n"));
659 let variants: Vec<&str> = group.iter().map(|entry| entry.variant).collect();
660 out.push_str(&format!("- **Variant**: `{}`\n", variants.join("`, `")));
661 if let Some(quickfix) = group[0].quickfix {
662 out.push_str(&format!("- **Quickfix**: {quickfix}\n"));
663 }
664 if let Some(docs) = &group[0].docs {
665 out.push_str(&format!("- **Guide**: <{docs}>\n"));
666 }
667 for entry in &group {
668 out.push_str("\n**Example**:\n\n```text\n");
669 out.push_str(&entry.headline);
670 out.push_str("\n```\n\n");
671 out.push_str(&format!("> {}\n", entry.explanation));
672 }
673 }
674 out
675}
676
677pub fn catalog_json() -> String {
680 let items: Vec<serde_json::Value> = catalog()
681 .iter()
682 .map(|entry| {
683 serde_json::json!({
684 "code": entry.code,
685 "severity": entry.severity,
686 "variant": entry.variant,
687 "headline": entry.headline,
688 "explanation": entry.explanation,
689 "quickfix": entry.quickfix,
690 "docs": entry.docs,
691 })
692 })
693 .collect();
694 let doc = serde_json::json!({
695 "schema": "logos-errors/1",
696 "errors": items,
697 });
698 let mut text = serde_json::to_string_pretty(&doc).expect("the catalog serializes");
699 text.push('\n');
700 text
701}
702
703#[cfg(test)]
704#[path = "../../logicaffeine_tests/unit/logicaffeine_language/diagnostic/tests.rs"]
705mod tests;