1use std::collections::HashMap;
26use std::sync::LazyLock;
27
28#[derive(Clone, Debug)]
30pub struct ErrorExplanation {
31 pub code: &'static str,
33 pub title: &'static str,
35 pub explanation: &'static str,
37 pub example: Option<&'static str>,
39 pub correct_example: Option<&'static str>,
41 pub doc_link: Option<&'static str>,
43 pub related_codes: &'static [&'static str],
45 pub common_mistakes: &'static [CommonMistake],
47}
48
49#[derive(Clone, Debug)]
51pub struct CommonMistake {
52 pub pattern: &'static str,
54 pub fix: &'static str,
56}
57
58static ERROR_REGISTRY: LazyLock<HashMap<&'static str, ErrorExplanation>> = LazyLock::new(|| {
60 let mut map = HashMap::new();
61
62 map.insert(
65 "E0001",
66 ErrorExplanation {
67 code: "E0001",
68 title: "Type mismatch",
69 explanation: r#"
70This error occurs when the type checker expects one type but finds another.
71
72Type mismatches commonly occur when:
73- A function is called with an argument of the wrong type
74- A variable is used in a context that requires a different type
75- A return value doesn't match the function's declared return type
76
77The compiler shows the expected type and the actual type found. Check that
78your types align, or add explicit type conversions where needed.
79"#,
80 example: Some(
81 r#"
82foo :: Int -> Int
83foo x = "hello" -- Error: expected Int, found String
84"#,
85 ),
86 correct_example: Some(
87 r#"
88foo :: Int -> Int
89foo x = x + 1
90"#,
91 ),
92 doc_link: Some("https://bhc.dev/docs/type-system"),
93 related_codes: &["E0007", "E0009"],
94 common_mistakes: &[
95 CommonMistake {
96 pattern: "Returning wrong type from function",
97 fix: "Check the function's type signature matches the return expression",
98 },
99 CommonMistake {
100 pattern: "Passing string literal where number expected",
101 fix: "Use numeric literals (42) not strings (\"42\")",
102 },
103 ],
104 },
105 );
106
107 map.insert(
108 "E0002",
109 ErrorExplanation {
110 code: "E0002",
111 title: "Infinite type (occurs check)",
112 explanation: r#"
113This error occurs when type inference would create an infinite type,
114typically when a value is used in a way that would require it to contain
115itself.
116
117This is detected by the "occurs check" during unification. If a type
118variable would need to be unified with a type containing that same
119variable, it would create an infinite loop in the type.
120
121Common causes:
122- Recursive data without proper type annotations
123- Accidentally creating circular references in types
124"#,
125 example: Some(
126 r#"
127-- This creates an infinite type: a = [a]
128foo x = [x, foo x]
129"#,
130 ),
131 correct_example: Some(
132 r#"
133-- Use a recursive data type instead
134data Tree a = Leaf a | Node [Tree a]
135
136foo :: a -> Tree a
137foo x = Node [Leaf x, foo x]
138"#,
139 ),
140 doc_link: Some("https://bhc.dev/docs/type-inference"),
141 related_codes: &["E0001", "E0022"],
142 common_mistakes: &[CommonMistake {
143 pattern: "Building a list containing the result of a recursive call",
144 fix: "Use a proper recursive data type instead of lists",
145 }],
146 },
147 );
148
149 map.insert(
150 "E0003",
151 ErrorExplanation {
152 code: "E0003",
153 title: "Unbound variable",
154 explanation: r#"
155This error occurs when you use a variable name that hasn't been defined
156in the current scope.
157
158Common causes:
159- Typo in the variable name
160- Using a variable before it's defined
161- Variable defined in a different scope (e.g., inside a let or lambda)
162- Forgot to import a module
163
164The compiler will suggest similar names if it finds a likely typo.
165"#,
166 example: Some(
167 r#"
168foo = x + 1 -- Error: x is not defined
169"#,
170 ),
171 correct_example: Some(
172 r#"
173foo x = x + 1 -- x is now a parameter
174"#,
175 ),
176 doc_link: Some("https://bhc.dev/docs/scoping"),
177 related_codes: &["E0004"],
178 common_mistakes: &[
179 CommonMistake {
180 pattern: "Typo in variable name (e.g., 'lenght' instead of 'length')",
181 fix: "Check the 'did you mean?' suggestion in the error message",
182 },
183 CommonMistake {
184 pattern: "Using a variable from an inner scope",
185 fix: "Pass the variable as a parameter or define it in the current scope",
186 },
187 CommonMistake {
188 pattern: "Forgot to import a module",
189 fix: "Add an import statement for the missing module",
190 },
191 ],
192 },
193 );
194
195 map.insert(
196 "E0004",
197 ErrorExplanation {
198 code: "E0004",
199 title: "Unbound constructor",
200 explanation: r#"
201This error occurs when you use a data constructor that hasn't been defined
202or imported.
203
204Data constructors in Haskell/BHC must:
205- Start with an uppercase letter (e.g., Just, Nothing, True)
206- Be defined in a data/newtype declaration
207- Be imported if defined in another module
208
209Common causes:
210- Typo in the constructor name
211- Forgot to import the data type
212- Constructor is not exported from its module
213"#,
214 example: Some(
215 r#"
216foo = Jus 42 -- Error: typo, should be Just
217"#,
218 ),
219 correct_example: Some(
220 r#"
221foo = Just 42 -- Correct constructor name
222"#,
223 ),
224 doc_link: Some("https://bhc.dev/docs/data-types"),
225 related_codes: &["E0003", "E0005"],
226 common_mistakes: &[
227 CommonMistake {
228 pattern: "Typo in constructor name",
229 fix: "Check the 'did you mean?' suggestion",
230 },
231 CommonMistake {
232 pattern: "Constructor not exported",
233 fix: "Import the module with explicit constructor list",
234 },
235 ],
236 },
237 );
238
239 map.insert(
240 "E0005",
241 ErrorExplanation {
242 code: "E0005",
243 title: "Pattern arity mismatch",
244 explanation: r#"
245This error occurs when a pattern has a different number of arguments than
246the data constructor expects.
247
248Each data constructor has a fixed number of fields. When pattern matching,
249you must provide exactly that many pattern variables.
250"#,
251 example: Some(
252 r#"
253data Point = Point Int Int
254
255foo (Point x) = x -- Error: Point has 2 fields, but pattern has 1
256"#,
257 ),
258 correct_example: Some(
259 r#"
260data Point = Point Int Int
261
262foo (Point x y) = x + y -- Correct: matches both fields
263-- Or use a wildcard:
264foo (Point x _) = x -- Ignore second field
265"#,
266 ),
267 doc_link: Some("https://bhc.dev/docs/pattern-matching"),
268 related_codes: &["E0004", "E0008"],
269 common_mistakes: &[CommonMistake {
270 pattern: "Missing pattern variables",
271 fix: "Use wildcards (_) for fields you don't need",
272 }],
273 },
274 );
275
276 map.insert(
277 "E0006",
278 ErrorExplanation {
279 code: "E0006",
280 title: "Ambiguous type variable",
281 explanation: r#"
282This error occurs when the compiler cannot determine a concrete type for
283a type variable. The type is ambiguous because there's not enough
284information to resolve it.
285
286This often happens with:
287- Numeric literals that could be Int, Float, etc.
288- Polymorphic functions where the result type isn't constrained
289- Show/Read without a concrete type context
290
291Solution: Add a type annotation to specify the intended type.
292"#,
293 example: Some(
294 r#"
295foo = show (read "42") -- Error: ambiguous type for read
296"#,
297 ),
298 correct_example: Some(
299 r#"
300foo = show (read "42" :: Int) -- Explicit type annotation
301"#,
302 ),
303 doc_link: Some("https://bhc.dev/docs/type-inference"),
304 related_codes: &["E0001"],
305 common_mistakes: &[
306 CommonMistake {
307 pattern: "Using read without type annotation",
308 fix: "Add :: Type after the expression",
309 },
310 CommonMistake {
311 pattern: "Numeric literal in polymorphic context",
312 fix: "Add type annotation like (42 :: Int)",
313 },
314 ],
315 },
316 );
317
318 map.insert(
319 "E0007",
320 ErrorExplanation {
321 code: "E0007",
322 title: "Kind mismatch",
323 explanation: r#"
324This error occurs when a type is used with the wrong kind.
325
326Kinds classify types:
327- `*` (or `Type`): Concrete types like Int, Bool, [Char]
328- `* -> *`: Type constructors like Maybe, [], IO
329- `* -> * -> *`: Two-parameter type constructors like Either, (,)
330
331Kind errors often occur when:
332- Applying a concrete type as if it were a type constructor
333- Forgetting to apply a type constructor to its argument
334"#,
335 example: Some(
336 r#"
337foo :: Int Maybe -- Error: Int has kind *, not * -> *
338"#,
339 ),
340 correct_example: Some(
341 r#"
342foo :: Maybe Int -- Correct: Maybe :: * -> *, Int :: *
343"#,
344 ),
345 doc_link: Some("https://bhc.dev/docs/kinds"),
346 related_codes: &["E0001"],
347 common_mistakes: &[CommonMistake {
348 pattern: "Type arguments in wrong order",
349 fix: "Put type constructor before its argument",
350 }],
351 },
352 );
353
354 map.insert(
355 "E0008",
356 ErrorExplanation {
357 code: "E0008",
358 title: "Function arity mismatch",
359 explanation: r#"
360This error occurs when a function is called with the wrong number of
361arguments.
362
363While Haskell/BHC supports partial application (providing fewer arguments
364than expected), this error is raised when you provide MORE arguments than
365the function accepts.
366
367The error message shows which arguments are extra.
368"#,
369 example: Some(
370 r#"
371add :: Int -> Int -> Int
372add x y = x + y
373
374result = add 1 2 3 -- Error: add takes 2 arguments, got 3
375"#,
376 ),
377 correct_example: Some(
378 r#"
379add :: Int -> Int -> Int
380add x y = x + y
381
382result = add 1 2 -- Correct: 2 arguments
383"#,
384 ),
385 doc_link: Some("https://bhc.dev/docs/functions"),
386 related_codes: &["E0005", "E0009"],
387 common_mistakes: &[CommonMistake {
388 pattern: "Passing extra arguments",
389 fix: "Check the function's type signature for argument count",
390 }],
391 },
392 );
393
394 map.insert(
395 "E0009",
396 ErrorExplanation {
397 code: "E0009",
398 title: "Not a function",
399 explanation: r#"
400This error occurs when you try to apply something that isn't a function
401as if it were one.
402
403In Haskell/BHC, function application is denoted by juxtaposition:
404 f x -- Apply f to x
405
406If `f` is not a function type (doesn't have the form `a -> b`), you'll
407get this error.
408"#,
409 example: Some(
410 r#"
411x = 42
412result = x 10 -- Error: 42 is Int, not a function
413"#,
414 ),
415 correct_example: Some(
416 r#"
417f x = x + 1
418result = f 10 -- Correct: f is a function
419"#,
420 ),
421 doc_link: Some("https://bhc.dev/docs/functions"),
422 related_codes: &["E0001", "E0008"],
423 common_mistakes: &[
424 CommonMistake {
425 pattern: "Applying a non-function value",
426 fix: "Check if you meant to call a different function",
427 },
428 CommonMistake {
429 pattern: "Missing operator between values",
430 fix: "Add the operator like + or * between values",
431 },
432 ],
433 },
434 );
435
436 map.insert(
439 "E0020",
440 ErrorExplanation {
441 code: "E0020",
442 title: "Dimension mismatch",
443 explanation: r#"
444This error occurs when tensor dimensions don't match as required by an
445operation.
446
447For example, matrix multiplication requires the inner dimensions to match:
448 matmul :: Tensor '[m, k] a -> Tensor '[k, n] a -> Tensor '[m, n] a
449
450The 'k' dimension (columns of first matrix, rows of second) must be equal.
451
452Common causes:
453- Matrices with incompatible shapes for multiplication
454- Elementwise operations on tensors of different shapes
455- Incorrect reshape operations
456"#,
457 example: Some(
458 r#"
459-- Shapes: [3, 5] × [7, 4] - inner dimensions 5 ≠ 7
460result = matmul a b -- Error: dimension mismatch
461"#,
462 ),
463 correct_example: Some(
464 r#"
465-- Shapes: [3, 5] × [5, 4] - inner dimensions match
466result = matmul a b -- OK: produces [3, 4]
467"#,
468 ),
469 doc_link: Some("https://bhc.dev/docs/tensors/shapes"),
470 related_codes: &["E0023", "E0030", "E0031"],
471 common_mistakes: &[
472 CommonMistake {
473 pattern: "Matrices in wrong order for matmul",
474 fix: "Try swapping the arguments or transpose one matrix",
475 },
476 CommonMistake {
477 pattern: "Wrong reshape dimensions",
478 fix: "Check that total elements match before and after reshape",
479 },
480 ],
481 },
482 );
483
484 map.insert(
485 "E0023",
486 ErrorExplanation {
487 code: "E0023",
488 title: "Shape rank mismatch",
489 explanation: r#"
490This error occurs when a tensor has a different number of dimensions
491(rank) than expected.
492
493For example:
494- A function expecting a matrix (rank 2) receives a vector (rank 1)
495- A function expecting a vector (rank 1) receives a scalar (rank 0)
496
497Check that your tensors have the correct number of dimensions.
498"#,
499 example: Some(
500 r#"
501-- matmul expects rank-2 tensors (matrices)
502a :: Tensor '[10] Float -- rank 1 (vector)
503b :: Tensor '[10, 5] Float -- rank 2 (matrix)
504result = matmul a b -- Error: rank mismatch
505"#,
506 ),
507 correct_example: Some(
508 r#"
509-- Both operands are rank-2
510a :: Tensor '[1, 10] Float -- rank 2 (row vector as matrix)
511b :: Tensor '[10, 5] Float -- rank 2 (matrix)
512result = matmul a b -- OK: produces [1, 5]
513"#,
514 ),
515 doc_link: Some("https://bhc.dev/docs/tensors/ranks"),
516 related_codes: &["E0020", "E0030"],
517 common_mistakes: &[
518 CommonMistake {
519 pattern: "Vector where matrix expected",
520 fix: "Use reshape to add dimension: '[n] -> '[1, n] or '[n, 1]",
521 },
522 CommonMistake {
523 pattern: "Wrong tensor dimension count",
524 fix: "Use unsqueeze/squeeze to add/remove dimensions",
525 },
526 ],
527 },
528 );
529
530 map.insert(
533 "E0030",
534 ErrorExplanation {
535 code: "E0030",
536 title: "Matrix multiplication dimension mismatch",
537 explanation: r#"
538This error occurs specifically during matrix multiplication when the inner
539dimensions don't match.
540
541Matrix multiplication has the signature:
542 matmul :: Tensor '[m, k] a -> Tensor '[k, n] a -> Tensor '[m, n] a
543
544The second dimension of the first matrix (k, the number of columns) must
545equal the first dimension of the second matrix (k, the number of rows).
546
547Visual representation:
548 [m × k] @ [k × n] = [m × n]
549 └───┴──── these must match
550
551Common fixes:
552- Transpose one of the matrices
553- Swap the order of arguments
554- Reshape to get compatible dimensions
555"#,
556 example: Some(
557 r#"
558weights :: Tensor '[768, 512] Float
559input :: Tensor '[1024, 768] Float
560-- Error: matmul expects inner dims to match
561-- weights has 512 cols, input has 1024 rows
562result = matmul weights input
563"#,
564 ),
565 correct_example: Some(
566 r#"
567weights :: Tensor '[768, 512] Float
568input :: Tensor '[768, 1024] Float
569-- 768 == 768, so inner dimensions match
570result = matmul (transpose weights) input -- [512, 1024]
571
572-- Or swap the order:
573result = matmul input weights -- Depends on what you want
574"#,
575 ),
576 doc_link: Some("https://bhc.dev/docs/tensors/matmul"),
577 related_codes: &["E0020", "E0023", "E0038"],
578 common_mistakes: &[
579 CommonMistake {
580 pattern: "Arguments in wrong order",
581 fix: "Try matmul b a instead of matmul a b",
582 },
583 CommonMistake {
584 pattern: "Forgot to transpose",
585 fix: "Use transpose on one of the matrices",
586 },
587 CommonMistake {
588 pattern: "Shapes are reversed from numpy/pytorch convention",
589 fix: "BHC uses [rows, cols] ordering consistently",
590 },
591 ],
592 },
593 );
594
595 map.insert(
596 "E0031",
597 ErrorExplanation {
598 code: "E0031",
599 title: "Broadcast incompatible shapes",
600 explanation: r#"
601This error occurs when two tensors have shapes that cannot be broadcast
602together according to NumPy-style broadcasting rules.
603
604Broadcasting rules:
6051. Shapes are compared element-wise from the trailing dimensions
6062. Dimensions are compatible if they are equal or one of them is 1
6073. Missing dimensions are treated as 1
608
609For example:
610 [3, 4] and [4] -> OK (becomes [3, 4])
611 [3, 4] and [1, 4] -> OK (becomes [3, 4])
612 [3, 4] and [2, 4] -> Error! (3 ≠ 2 and neither is 1)
613"#,
614 example: Some(
615 r#"
616a :: Tensor '[3, 4] Float
617b :: Tensor '[2, 4] Float
618result = a + b -- Error: cannot broadcast [3,4] with [2,4]
619"#,
620 ),
621 correct_example: Some(
622 r#"
623a :: Tensor '[3, 4] Float
624b :: Tensor '[1, 4] Float
625result = a + b -- OK: b broadcasts to [3, 4]
626"#,
627 ),
628 doc_link: Some("https://bhc.dev/docs/tensors/broadcasting"),
629 related_codes: &["E0020", "E0023"],
630 common_mistakes: &[
631 CommonMistake {
632 pattern: "Non-1 dimensions that don't match",
633 fix: "Use reshape to make one dimension 1 for broadcasting",
634 },
635 CommonMistake {
636 pattern: "Broadcasting where elementwise was intended",
637 fix: "Ensure shapes match exactly or use explicit broadcast",
638 },
639 ],
640 },
641 );
642
643 map.insert(
644 "E0037",
645 ErrorExplanation {
646 code: "E0037",
647 title: "Dynamic tensor conversion failed",
648 explanation: r#"
649This error occurs when attempting to convert a DynTensor to a statically-
650shaped tensor with fromDynamic, but the runtime shape doesn't match.
651
652DynTensor is an existentially-quantified wrapper that hides the shape:
653 data DynTensor a where
654 MkDynTensor :: Tensor shape a -> DynTensor a
655
656When using fromDynamic, you must handle the case where the shapes don't
657match:
658 fromDynamic :: ShapeWitness shape -> DynTensor a -> Maybe (Tensor shape a)
659
660Always pattern match on the Maybe result to handle both cases.
661"#,
662 example: Some(
663 r#"
664processTensor :: DynTensor Float -> Tensor '[256, 256] Float
665processTensor dyn = fromJust (fromDynamic witness dyn)
666-- Error if runtime shape isn't [256, 256]!
667"#,
668 ),
669 correct_example: Some(
670 r#"
671processTensor :: DynTensor Float -> Maybe (Tensor '[256, 256] Float)
672processTensor dyn = case fromDynamic witness dyn of
673 Just tensor -> Just (processStatic tensor)
674 Nothing -> Nothing -- Handle shape mismatch
675"#,
676 ),
677 doc_link: Some("https://bhc.dev/docs/tensors/dynamic"),
678 related_codes: &["E0020", "E0023"],
679 common_mistakes: &[
680 CommonMistake {
681 pattern: "Using fromJust with fromDynamic",
682 fix: "Always pattern match on Maybe result",
683 },
684 CommonMistake {
685 pattern: "Not validating runtime shapes",
686 fix: "Check shape at boundaries using fromDynamic safely",
687 },
688 ],
689 },
690 );
691
692 map.insert(
695 "W0001",
696 ErrorExplanation {
697 code: "W0001",
698 title: "Unused variable",
699 explanation: r#"
700This warning indicates that a variable is defined but never used.
701
702While this doesn't prevent compilation, unused variables often indicate:
703- Incomplete code (forgot to use the variable)
704- Dead code that can be removed
705- A typo in the variable name
706
707To suppress this warning for intentionally unused variables, prefix the
708name with an underscore: `_unused`.
709"#,
710 example: Some(
711 r#"
712foo x y = x + 1 -- Warning: y is unused
713"#,
714 ),
715 correct_example: Some(
716 r#"
717foo x _y = x + 1 -- No warning: _y is intentionally unused
718-- Or actually use y:
719foo x y = x + y
720"#,
721 ),
722 doc_link: Some("https://bhc.dev/docs/warnings"),
723 related_codes: &[],
724 common_mistakes: &[
725 CommonMistake {
726 pattern: "Forgot to use variable in computation",
727 fix: "Use the variable or prefix with _ if intentionally unused",
728 },
729 CommonMistake {
730 pattern: "Typo in variable name causing apparent unused var",
731 fix: "Check for similar names used elsewhere",
732 },
733 ],
734 },
735 );
736
737 map
738});
739
740#[must_use]
742pub fn get_explanation(code: &str) -> Option<&'static ErrorExplanation> {
743 ERROR_REGISTRY.get(code)
744}
745
746#[must_use]
748pub fn all_error_codes() -> Vec<&'static str> {
749 let mut codes: Vec<_> = ERROR_REGISTRY.keys().copied().collect();
750 codes.sort();
751 codes
752}
753
754#[must_use]
756pub fn format_explanation(explanation: &ErrorExplanation) -> String {
757 let mut output = String::new();
758
759 output.push_str(&format!(
760 "# {} - {}\n\n",
761 explanation.code, explanation.title
762 ));
763 output.push_str(explanation.explanation.trim());
764 output.push_str("\n\n");
765
766 if let Some(example) = explanation.example {
767 output.push_str("## Example of erroneous code:\n");
768 output.push_str("```haskell");
769 output.push_str(example);
770 output.push_str("```\n\n");
771 }
772
773 if let Some(correct) = explanation.correct_example {
774 output.push_str("## Corrected code:\n");
775 output.push_str("```haskell");
776 output.push_str(correct);
777 output.push_str("```\n\n");
778 }
779
780 if !explanation.common_mistakes.is_empty() {
782 output.push_str("## Common Mistakes\n\n");
783 for mistake in explanation.common_mistakes {
784 output.push_str(&format!("**{}**\n", mistake.pattern));
785 output.push_str(&format!(" Fix: {}\n\n", mistake.fix));
786 }
787 }
788
789 if !explanation.related_codes.is_empty() {
791 output.push_str("## Related\n\n");
792 output.push_str("See also: ");
793 let codes: Vec<String> = explanation
794 .related_codes
795 .iter()
796 .map(|c| format!("`{}`", c))
797 .collect();
798 output.push_str(&codes.join(", "));
799 output.push_str("\n\n");
800 }
801
802 if let Some(doc_link) = explanation.doc_link {
804 output.push_str("## Documentation\n\n");
805 output.push_str(&format!("For more information, see: {}\n", doc_link));
806 }
807
808 output
809}
810
811pub fn print_explanation(code: &str) {
813 match get_explanation(code) {
814 Some(explanation) => {
815 println!("{}", format_explanation(explanation));
816 }
817 None => {
818 println!("Error code `{code}` not found.");
819 println!("\nAvailable error codes:");
820 for code in all_error_codes() {
821 if let Some(exp) = get_explanation(code) {
822 println!(" {}: {}", code, exp.title);
823 }
824 }
825 }
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 #[test]
834 fn test_get_explanation() {
835 let exp = get_explanation("E0001").unwrap();
836 assert_eq!(exp.code, "E0001");
837 assert_eq!(exp.title, "Type mismatch");
838 }
839
840 #[test]
841 fn test_unknown_code() {
842 assert!(get_explanation("E9999").is_none());
843 }
844
845 #[test]
846 fn test_all_error_codes() {
847 let codes = all_error_codes();
848 assert!(!codes.is_empty());
849 assert!(codes.contains(&"E0001"));
850 }
851
852 #[test]
853 fn test_format_explanation() {
854 let exp = get_explanation("E0001").unwrap();
855 let formatted = format_explanation(exp);
856 assert!(formatted.contains("E0001"));
857 assert!(formatted.contains("Type mismatch"));
858 }
859}