Skip to main content

bhc_diagnostics/
explain.rs

1//! Error code explanations.
2//!
3//! This module provides detailed explanations for error codes, accessible
4//! via the `--explain` flag (e.g., `bhc --explain E0001`).
5//!
6//! ## Error Code Conventions
7//!
8//! - `E0xxx`: Type errors
9//! - `E00xx`: Basic type mismatches (E0001-E0019)
10//! - `E002x`: Shape/dimension errors (E0020-E0039)
11//! - `E003x`: Tensor operation errors (E0030-E0039)
12//! - `E004x`: Pattern matching errors (E0040-E0049)
13//! - `E005x`: Module/import errors (E0050-E0059)
14//! - `W0xxx`: Warnings
15//! - `W001x`: Unused bindings (W0010-W0019)
16//! - `W002x`: Deprecated features (W0020-W0029)
17//!
18//! ## M10 Phase 4: Contextual Help
19//!
20//! Each error explanation includes:
21//! - Documentation links to relevant sections
22//! - Related error codes for similar issues
23//! - Common mistake patterns with fixes
24
25use std::collections::HashMap;
26use std::sync::LazyLock;
27
28/// An error code explanation.
29#[derive(Clone, Debug)]
30pub struct ErrorExplanation {
31    /// The error code.
32    pub code: &'static str,
33    /// A brief title for the error.
34    pub title: &'static str,
35    /// A detailed explanation.
36    pub explanation: &'static str,
37    /// Example code that triggers this error.
38    pub example: Option<&'static str>,
39    /// Example of the correct code.
40    pub correct_example: Option<&'static str>,
41    /// Link to related documentation.
42    pub doc_link: Option<&'static str>,
43    /// Related error codes that might be relevant.
44    pub related_codes: &'static [&'static str],
45    /// Common mistake patterns that lead to this error.
46    pub common_mistakes: &'static [CommonMistake],
47}
48
49/// A common mistake pattern with its fix.
50#[derive(Clone, Debug)]
51pub struct CommonMistake {
52    /// Description of the mistake pattern.
53    pub pattern: &'static str,
54    /// Suggested fix for this mistake.
55    pub fix: &'static str,
56}
57
58/// Registry of all error code explanations.
59static ERROR_REGISTRY: LazyLock<HashMap<&'static str, ErrorExplanation>> = LazyLock::new(|| {
60    let mut map = HashMap::new();
61
62    // === Type Errors (E0001-E0019) ===
63
64    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    // === Shape Errors (E0020-E0029) ===
437
438    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    // === Tensor Operation Errors (E0030-E0039) ===
531
532    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    // === Warnings (W0xxx) ===
693
694    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/// Look up an error explanation by code.
741#[must_use]
742pub fn get_explanation(code: &str) -> Option<&'static ErrorExplanation> {
743    ERROR_REGISTRY.get(code)
744}
745
746/// Get all registered error codes.
747#[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/// Format an error explanation for display.
755#[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    // M10 Phase 4: Common mistakes
781    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    // M10 Phase 4: Related error codes
790    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    // M10 Phase 4: Documentation link
803    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
811/// Print an error explanation to stdout.
812pub 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}