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