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: &[
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    // === Shape Errors (E0020-E0029) ===
445
446    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    // === Tensor Operation Errors (E0030-E0039) ===
539
540    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    // === Warnings (W0xxx) ===
701
702    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/// Look up an error explanation by code.
749#[must_use]
750pub fn get_explanation(code: &str) -> Option<&'static ErrorExplanation> {
751    ERROR_REGISTRY.get(code)
752}
753
754/// Get all registered error codes.
755#[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/// Format an error explanation for display.
763#[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    // M10 Phase 4: Common mistakes
786    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    // M10 Phase 4: Related error codes
795    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    // M10 Phase 4: Documentation link
807    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
815/// Print an error explanation to stdout.
816pub 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}