Skip to main content

tensorlogic_compiler/passes/
diagnostics.rs

1//! Enhanced error diagnostics and messaging.
2
3use anyhow::{anyhow, Result};
4use tensorlogic_ir::{IrError, SourceSpan, TLExpr, Term};
5
6use super::scope_analysis::analyze_scopes;
7
8/// Diagnostic level
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DiagnosticLevel {
11    Error,
12    Warning,
13    Info,
14    Hint,
15}
16
17/// A diagnostic message with location and context
18#[derive(Debug, Clone)]
19pub struct Diagnostic {
20    pub level: DiagnosticLevel,
21    pub message: String,
22    pub span: Option<SourceSpan>,
23    pub help: Option<String>,
24    pub related: Vec<(String, Option<SourceSpan>)>,
25}
26
27impl Diagnostic {
28    pub fn error(message: impl Into<String>) -> Self {
29        Diagnostic {
30            level: DiagnosticLevel::Error,
31            message: message.into(),
32            span: None,
33            help: None,
34            related: Vec::new(),
35        }
36    }
37
38    pub fn warning(message: impl Into<String>) -> Self {
39        Diagnostic {
40            level: DiagnosticLevel::Warning,
41            message: message.into(),
42            span: None,
43            help: None,
44            related: Vec::new(),
45        }
46    }
47
48    pub fn with_span(mut self, span: SourceSpan) -> Self {
49        self.span = Some(span);
50        self
51    }
52
53    pub fn with_help(mut self, help: impl Into<String>) -> Self {
54        self.help = Some(help.into());
55        self
56    }
57
58    pub fn with_related(mut self, msg: impl Into<String>, span: Option<SourceSpan>) -> Self {
59        self.related.push((msg.into(), span));
60        self
61    }
62
63    /// Format the diagnostic for display
64    pub fn format(&self) -> String {
65        let mut output = String::new();
66
67        let level_str = match self.level {
68            DiagnosticLevel::Error => "error",
69            DiagnosticLevel::Warning => "warning",
70            DiagnosticLevel::Info => "info",
71            DiagnosticLevel::Hint => "hint",
72        };
73
74        if let Some(ref span) = self.span {
75            output.push_str(&format!("{}: {}: {}\n", level_str, span, self.message));
76        } else {
77            output.push_str(&format!("{}: {}\n", level_str, self.message));
78        }
79
80        if let Some(ref help) = self.help {
81            output.push_str(&format!("  help: {}\n", help));
82        }
83
84        for (msg, span_opt) in &self.related {
85            if let Some(span) = span_opt {
86                output.push_str(&format!("  note: {}: {}\n", span, msg));
87            } else {
88                output.push_str(&format!("  note: {}\n", msg));
89            }
90        }
91
92        output
93    }
94}
95
96/// Enhanced error message builder
97pub struct DiagnosticBuilder {
98    diagnostics: Vec<Diagnostic>,
99}
100
101impl DiagnosticBuilder {
102    pub fn new() -> Self {
103        DiagnosticBuilder {
104            diagnostics: Vec::new(),
105        }
106    }
107
108    pub fn add(&mut self, diagnostic: Diagnostic) {
109        self.diagnostics.push(diagnostic);
110    }
111
112    pub fn has_errors(&self) -> bool {
113        self.diagnostics
114            .iter()
115            .any(|d| d.level == DiagnosticLevel::Error)
116    }
117
118    pub fn error_count(&self) -> usize {
119        self.diagnostics
120            .iter()
121            .filter(|d| d.level == DiagnosticLevel::Error)
122            .count()
123    }
124
125    pub fn into_result(self) -> Result<()> {
126        if self.has_errors() {
127            let mut msg = String::new();
128            for diag in &self.diagnostics {
129                msg.push_str(&diag.format());
130            }
131            Err(anyhow!("{}", msg))
132        } else {
133            Ok(())
134        }
135    }
136
137    pub fn diagnostics(&self) -> &[Diagnostic] {
138        &self.diagnostics
139    }
140}
141
142impl Default for DiagnosticBuilder {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148/// Enhance an IrError with better diagnostics
149pub fn enhance_error(error: IrError) -> Diagnostic {
150    match error {
151        IrError::ArityMismatch {
152            name,
153            expected,
154            actual,
155        } => Diagnostic::error(format!(
156            "Predicate '{}' arity mismatch: expected {} arguments, got {}",
157            name, expected, actual
158        ))
159        .with_help(format!(
160            "Change the number of arguments to match the expected arity of {}",
161            expected
162        )),
163        IrError::TypeMismatch {
164            name,
165            arg_index,
166            expected,
167            actual,
168        } => Diagnostic::error(format!(
169            "Type mismatch in predicate '{}' at argument {}: expected '{}', got '{}'",
170            name, arg_index, expected, actual
171        ))
172        .with_help(format!(
173            "Change argument {} to have type '{}'",
174            arg_index, expected
175        )),
176        IrError::UnboundVariable { var } => {
177            Diagnostic::error(format!("Variable '{}' is not bound by any quantifier", var))
178                .with_help(format!(
179                    "Add a quantifier: ∀{}. <expr> or ∃{}. <expr>",
180                    var, var
181                ))
182        }
183        IrError::InconsistentTypes { var, type1, type2 } => Diagnostic::error(format!(
184            "Variable '{}' used with inconsistent types: '{}' and '{}'",
185            var, type1, type2
186        ))
187        .with_help("Ensure the variable has the same type in all uses".to_string()),
188        _ => Diagnostic::error(format!("{}", error)),
189    }
190}
191
192/// Generate diagnostics for an expression
193pub fn diagnose_expression(expr: &TLExpr) -> Vec<Diagnostic> {
194    let mut diagnostics = Vec::new();
195
196    // Check for unbound variables
197    if let Ok(scope_result) = analyze_scopes(expr) {
198        for unbound_var in &scope_result.unbound_variables {
199            let diag = Diagnostic::error(format!("Unbound variable '{}'", unbound_var)).with_help(
200                format!(
201                    "Consider adding a universal quantifier: ∀{}. <expr>",
202                    unbound_var
203                ),
204            );
205            diagnostics.push(diag);
206        }
207
208        // Check for type conflicts
209        for conflict in &scope_result.type_conflicts {
210            let diag = Diagnostic::error(format!(
211                "Variable '{}' has conflicting types: '{}' and '{}'",
212                conflict.variable, conflict.type1, conflict.type2
213            ))
214            .with_help("Ensure the variable has consistent types across all uses".to_string());
215            diagnostics.push(diag);
216        }
217    }
218
219    // Warn about variables bound but never used
220    diagnose_unused_bindings(expr, &mut diagnostics);
221
222    diagnostics
223}
224
225fn diagnose_unused_bindings(expr: &TLExpr, diagnostics: &mut Vec<Diagnostic>) {
226    match expr {
227        TLExpr::Exists {
228            var,
229            domain: _,
230            body,
231        }
232        | TLExpr::ForAll {
233            var,
234            domain: _,
235            body,
236        }
237        | TLExpr::SoftExists {
238            var,
239            domain: _,
240            body,
241            ..
242        }
243        | TLExpr::SoftForAll {
244            var,
245            domain: _,
246            body,
247            ..
248        }
249        | TLExpr::Aggregate {
250            var,
251            domain: _,
252            body,
253            ..
254        } => {
255            // Check if variable is actually used in the body
256            if !uses_variable(body, var) {
257                diagnostics.push(
258                    Diagnostic::warning(format!("Variable '{}' is bound but never used", var))
259                        .with_help(format!("Consider removing the quantifier for '{}'", var)),
260                );
261            }
262            diagnose_unused_bindings(body, diagnostics);
263        }
264        TLExpr::And(left, right)
265        | TLExpr::Or(left, right)
266        | TLExpr::Imply(left, right)
267        | TLExpr::Add(left, right)
268        | TLExpr::Sub(left, right)
269        | TLExpr::Mul(left, right)
270        | TLExpr::Div(left, right)
271        | TLExpr::Pow(left, right)
272        | TLExpr::Mod(left, right)
273        | TLExpr::Min(left, right)
274        | TLExpr::Max(left, right)
275        | TLExpr::Eq(left, right)
276        | TLExpr::Lt(left, right)
277        | TLExpr::Gt(left, right)
278        | TLExpr::Lte(left, right)
279        | TLExpr::Gte(left, right)
280        | TLExpr::TNorm { left, right, .. }
281        | TLExpr::TCoNorm { left, right, .. }
282        | TLExpr::FuzzyImplication {
283            premise: left,
284            conclusion: right,
285            ..
286        } => {
287            diagnose_unused_bindings(left, diagnostics);
288            diagnose_unused_bindings(right, diagnostics);
289        }
290        TLExpr::Not(inner)
291        | TLExpr::Score(inner)
292        | TLExpr::Abs(inner)
293        | TLExpr::Floor(inner)
294        | TLExpr::Ceil(inner)
295        | TLExpr::Round(inner)
296        | TLExpr::Sqrt(inner)
297        | TLExpr::Exp(inner)
298        | TLExpr::Log(inner)
299        | TLExpr::Sin(inner)
300        | TLExpr::Cos(inner)
301        | TLExpr::Tan(inner)
302        | TLExpr::FuzzyNot { expr: inner, .. }
303        | TLExpr::WeightedRule { rule: inner, .. } => {
304            diagnose_unused_bindings(inner, diagnostics);
305        }
306        TLExpr::Let {
307            var: _,
308            value,
309            body,
310        } => {
311            diagnose_unused_bindings(value, diagnostics);
312            diagnose_unused_bindings(body, diagnostics);
313        }
314        TLExpr::IfThenElse {
315            condition,
316            then_branch,
317            else_branch,
318        } => {
319            diagnose_unused_bindings(condition, diagnostics);
320            diagnose_unused_bindings(then_branch, diagnostics);
321            diagnose_unused_bindings(else_branch, diagnostics);
322        }
323
324        // Modal/temporal logic operators - not yet implemented, pass through with recursion
325        TLExpr::Box(inner)
326        | TLExpr::Diamond(inner)
327        | TLExpr::Next(inner)
328        | TLExpr::Eventually(inner)
329        | TLExpr::Always(inner) => {
330            diagnose_unused_bindings(inner, diagnostics);
331        }
332        TLExpr::Until { before, after }
333        | TLExpr::Release {
334            released: before,
335            releaser: after,
336        }
337        | TLExpr::WeakUntil { before, after }
338        | TLExpr::StrongRelease {
339            released: before,
340            releaser: after,
341        } => {
342            diagnose_unused_bindings(before, diagnostics);
343            diagnose_unused_bindings(after, diagnostics);
344        }
345        TLExpr::ProbabilisticChoice { alternatives } => {
346            for (_weight, alt_expr) in alternatives {
347                diagnose_unused_bindings(alt_expr, diagnostics);
348            }
349        }
350
351        TLExpr::Pred { .. } => {}
352        TLExpr::Constant(_) => {}
353        // All other expression types (enhancements)
354        _ => {}
355    }
356}
357
358fn uses_variable(expr: &TLExpr, var_name: &str) -> bool {
359    match expr {
360        TLExpr::Pred { name: _, args } => args.iter().any(|term| match term {
361            Term::Var(v) => v == var_name,
362            Term::Typed { value, .. } => uses_variable_in_term(value, var_name),
363            _ => false,
364        }),
365        TLExpr::And(left, right)
366        | TLExpr::Or(left, right)
367        | TLExpr::Imply(left, right)
368        | TLExpr::Add(left, right)
369        | TLExpr::Sub(left, right)
370        | TLExpr::Mul(left, right)
371        | TLExpr::Div(left, right)
372        | TLExpr::Pow(left, right)
373        | TLExpr::Mod(left, right)
374        | TLExpr::Min(left, right)
375        | TLExpr::Max(left, right)
376        | TLExpr::Eq(left, right)
377        | TLExpr::Lt(left, right)
378        | TLExpr::Gt(left, right)
379        | TLExpr::Lte(left, right)
380        | TLExpr::Gte(left, right)
381        | TLExpr::TNorm { left, right, .. }
382        | TLExpr::TCoNorm { left, right, .. }
383        | TLExpr::FuzzyImplication {
384            premise: left,
385            conclusion: right,
386            ..
387        } => uses_variable(left, var_name) || uses_variable(right, var_name),
388        TLExpr::Not(inner)
389        | TLExpr::Score(inner)
390        | TLExpr::Abs(inner)
391        | TLExpr::Floor(inner)
392        | TLExpr::Ceil(inner)
393        | TLExpr::Round(inner)
394        | TLExpr::Sqrt(inner)
395        | TLExpr::Exp(inner)
396        | TLExpr::Log(inner)
397        | TLExpr::Sin(inner)
398        | TLExpr::Cos(inner)
399        | TLExpr::Tan(inner)
400        | TLExpr::FuzzyNot { expr: inner, .. }
401        | TLExpr::WeightedRule { rule: inner, .. } => uses_variable(inner, var_name),
402        TLExpr::Let {
403            var: _,
404            value,
405            body,
406        } => uses_variable(value, var_name) || uses_variable(body, var_name),
407        TLExpr::Exists {
408            var: _,
409            domain: _,
410            body,
411        }
412        | TLExpr::ForAll {
413            var: _,
414            domain: _,
415            body,
416        }
417        | TLExpr::SoftExists {
418            var: _,
419            domain: _,
420            body,
421            ..
422        }
423        | TLExpr::SoftForAll {
424            var: _,
425            domain: _,
426            body,
427            ..
428        }
429        | TLExpr::Aggregate {
430            var: _,
431            domain: _,
432            body,
433            ..
434        } => uses_variable(body, var_name),
435        TLExpr::IfThenElse {
436            condition,
437            then_branch,
438            else_branch,
439        } => {
440            uses_variable(condition, var_name)
441                || uses_variable(then_branch, var_name)
442                || uses_variable(else_branch, var_name)
443        }
444
445        // Modal/temporal logic operators - not yet implemented, pass through with recursion
446        TLExpr::Box(inner)
447        | TLExpr::Diamond(inner)
448        | TLExpr::Next(inner)
449        | TLExpr::Eventually(inner)
450        | TLExpr::Always(inner) => uses_variable(inner, var_name),
451        TLExpr::Until { before, after }
452        | TLExpr::Release {
453            released: before,
454            releaser: after,
455        }
456        | TLExpr::WeakUntil { before, after }
457        | TLExpr::StrongRelease {
458            released: before,
459            releaser: after,
460        } => uses_variable(before, var_name) || uses_variable(after, var_name),
461        TLExpr::ProbabilisticChoice { alternatives } => alternatives
462            .iter()
463            .any(|(_weight, alt_expr)| uses_variable(alt_expr, var_name)),
464
465        TLExpr::Constant(_) => false,
466        // All other expression types (enhancements)
467        _ => false,
468    }
469}
470
471fn uses_variable_in_term(term: &Term, var_name: &str) -> bool {
472    match term {
473        Term::Var(v) => v == var_name,
474        Term::Typed { value, .. } => uses_variable_in_term(value, var_name),
475        _ => false,
476    }
477}
478
479/// Pretty-print an expression for error messages
480pub fn pretty_print_expr(expr: &TLExpr) -> String {
481    match expr {
482        TLExpr::Pred { name, args } => {
483            if args.is_empty() {
484                name.clone()
485            } else {
486                let args_str = args
487                    .iter()
488                    .map(pretty_print_term)
489                    .collect::<Vec<_>>()
490                    .join(", ");
491                format!("{}({})", name, args_str)
492            }
493        }
494        TLExpr::And(left, right) => {
495            format!(
496                "({} ∧ {})",
497                pretty_print_expr(left),
498                pretty_print_expr(right)
499            )
500        }
501        TLExpr::Or(left, right) => {
502            format!(
503                "({} ∨ {})",
504                pretty_print_expr(left),
505                pretty_print_expr(right)
506            )
507        }
508        TLExpr::Not(inner) => format!("¬{}", pretty_print_expr(inner)),
509        TLExpr::Imply(premise, conclusion) => {
510            format!(
511                "({} → {})",
512                pretty_print_expr(premise),
513                pretty_print_expr(conclusion)
514            )
515        }
516        TLExpr::Exists { var, domain, body } => {
517            format!("∃{}:{}. {}", var, domain, pretty_print_expr(body))
518        }
519        TLExpr::ForAll { var, domain, body } => {
520            format!("∀{}:{}. {}", var, domain, pretty_print_expr(body))
521        }
522        TLExpr::Aggregate {
523            op,
524            var,
525            domain,
526            body,
527            group_by,
528        } => {
529            let group_str = if let Some(gb) = group_by {
530                format!(" GROUP BY {}", gb.join(", "))
531            } else {
532                String::new()
533            };
534            format!(
535                "AGG[{}]({}:{}. {}){}",
536                op,
537                var,
538                domain,
539                pretty_print_expr(body),
540                group_str
541            )
542        }
543        TLExpr::Score(inner) => format!("score({})", pretty_print_expr(inner)),
544        TLExpr::Add(left, right) => {
545            format!(
546                "({} + {})",
547                pretty_print_expr(left),
548                pretty_print_expr(right)
549            )
550        }
551        TLExpr::Sub(left, right) => {
552            format!(
553                "({} - {})",
554                pretty_print_expr(left),
555                pretty_print_expr(right)
556            )
557        }
558        TLExpr::Mul(left, right) => {
559            format!(
560                "({} * {})",
561                pretty_print_expr(left),
562                pretty_print_expr(right)
563            )
564        }
565        TLExpr::Div(left, right) => {
566            format!(
567                "({} / {})",
568                pretty_print_expr(left),
569                pretty_print_expr(right)
570            )
571        }
572        TLExpr::Eq(left, right) => {
573            format!(
574                "({} = {})",
575                pretty_print_expr(left),
576                pretty_print_expr(right)
577            )
578        }
579        TLExpr::Lt(left, right) => {
580            format!(
581                "({} < {})",
582                pretty_print_expr(left),
583                pretty_print_expr(right)
584            )
585        }
586        TLExpr::Gt(left, right) => {
587            format!(
588                "({} > {})",
589                pretty_print_expr(left),
590                pretty_print_expr(right)
591            )
592        }
593        TLExpr::Lte(left, right) => {
594            format!(
595                "({} ≤ {})",
596                pretty_print_expr(left),
597                pretty_print_expr(right)
598            )
599        }
600        TLExpr::Gte(left, right) => {
601            format!(
602                "({} ≥ {})",
603                pretty_print_expr(left),
604                pretty_print_expr(right)
605            )
606        }
607        TLExpr::Pow(left, right) => {
608            format!(
609                "({} ^ {})",
610                pretty_print_expr(left),
611                pretty_print_expr(right)
612            )
613        }
614        TLExpr::Mod(left, right) => {
615            format!(
616                "({} % {})",
617                pretty_print_expr(left),
618                pretty_print_expr(right)
619            )
620        }
621        TLExpr::Min(left, right) => {
622            format!(
623                "min({}, {})",
624                pretty_print_expr(left),
625                pretty_print_expr(right)
626            )
627        }
628        TLExpr::Max(left, right) => {
629            format!(
630                "max({}, {})",
631                pretty_print_expr(left),
632                pretty_print_expr(right)
633            )
634        }
635        TLExpr::Abs(inner) => format!("abs({})", pretty_print_expr(inner)),
636        TLExpr::Floor(inner) => format!("floor({})", pretty_print_expr(inner)),
637        TLExpr::Ceil(inner) => format!("ceil({})", pretty_print_expr(inner)),
638        TLExpr::Round(inner) => format!("round({})", pretty_print_expr(inner)),
639        TLExpr::Sqrt(inner) => format!("sqrt({})", pretty_print_expr(inner)),
640        TLExpr::Exp(inner) => format!("exp({})", pretty_print_expr(inner)),
641        TLExpr::Log(inner) => format!("log({})", pretty_print_expr(inner)),
642        TLExpr::Sin(inner) => format!("sin({})", pretty_print_expr(inner)),
643        TLExpr::Cos(inner) => format!("cos({})", pretty_print_expr(inner)),
644        TLExpr::Tan(inner) => format!("tan({})", pretty_print_expr(inner)),
645        TLExpr::Let { var, value, body } => {
646            format!(
647                "let {} = {} in {}",
648                var,
649                pretty_print_expr(value),
650                pretty_print_expr(body)
651            )
652        }
653        TLExpr::IfThenElse {
654            condition,
655            then_branch,
656            else_branch,
657        } => {
658            format!(
659                "if {} then {} else {}",
660                pretty_print_expr(condition),
661                pretty_print_expr(then_branch),
662                pretty_print_expr(else_branch)
663            )
664        }
665
666        // Modal/temporal logic operators
667        TLExpr::Box(inner) => format!("□{}", pretty_print_expr(inner)),
668        TLExpr::Diamond(inner) => format!("◇{}", pretty_print_expr(inner)),
669        TLExpr::Next(inner) => format!("X{}", pretty_print_expr(inner)),
670        TLExpr::Eventually(inner) => format!("F{}", pretty_print_expr(inner)),
671        TLExpr::Always(inner) => format!("G{}", pretty_print_expr(inner)),
672        TLExpr::Until { before, after } => {
673            format!(
674                "({} U {})",
675                pretty_print_expr(before),
676                pretty_print_expr(after)
677            )
678        }
679
680        // Fuzzy logic operators
681        TLExpr::TNorm { kind, left, right } => {
682            format!(
683                "({} ⊗_{:?} {})",
684                pretty_print_expr(left),
685                kind,
686                pretty_print_expr(right)
687            )
688        }
689        TLExpr::TCoNorm { kind, left, right } => {
690            format!(
691                "({} ⊕_{:?} {})",
692                pretty_print_expr(left),
693                kind,
694                pretty_print_expr(right)
695            )
696        }
697        TLExpr::FuzzyNot { kind, expr } => {
698            format!("¬_{:?}({})", kind, pretty_print_expr(expr))
699        }
700        TLExpr::FuzzyImplication {
701            kind,
702            premise,
703            conclusion,
704        } => {
705            format!(
706                "({} →_{:?} {})",
707                pretty_print_expr(premise),
708                kind,
709                pretty_print_expr(conclusion)
710            )
711        }
712        TLExpr::SoftExists {
713            var,
714            domain,
715            body,
716            temperature,
717        } => {
718            format!(
719                "∃{}:{}[T={}]. {}",
720                var,
721                domain,
722                temperature,
723                pretty_print_expr(body)
724            )
725        }
726        TLExpr::SoftForAll {
727            var,
728            domain,
729            body,
730            temperature,
731        } => {
732            format!(
733                "∀{}:{}[T={}]. {}",
734                var,
735                domain,
736                temperature,
737                pretty_print_expr(body)
738            )
739        }
740        TLExpr::WeightedRule { weight, rule } => {
741            format!("{}::{}", weight, pretty_print_expr(rule))
742        }
743        TLExpr::ProbabilisticChoice { alternatives } => {
744            let alt_strs: Vec<String> = alternatives
745                .iter()
746                .map(|(prob, expr)| format!("{}:{}", prob, pretty_print_expr(expr)))
747                .collect();
748            format!("CHOICE[{}]", alt_strs.join(", "))
749        }
750        TLExpr::Release { released, releaser } => {
751            format!(
752                "({} R {})",
753                pretty_print_expr(released),
754                pretty_print_expr(releaser)
755            )
756        }
757        TLExpr::WeakUntil { before, after } => {
758            format!(
759                "({} W {})",
760                pretty_print_expr(before),
761                pretty_print_expr(after)
762            )
763        }
764        TLExpr::StrongRelease { released, releaser } => {
765            format!(
766                "({} M {})",
767                pretty_print_expr(released),
768                pretty_print_expr(releaser)
769            )
770        }
771
772        TLExpr::Constant(value) => format!("{}", value),
773        // All other expression types (enhancements)
774        _ => "<expr>".to_string(),
775    }
776}
777
778fn pretty_print_term(term: &Term) -> String {
779    match term {
780        Term::Var(v) => v.clone(),
781        Term::Const(c) => c.clone(),
782        Term::Typed {
783            value,
784            type_annotation,
785        } => {
786            format!("{}:{}", pretty_print_term(value), type_annotation.type_name)
787        }
788    }
789}
790
791/// Create a detailed error message for common compilation errors
792pub fn create_detailed_error(
793    error_type: &str,
794    expr: &TLExpr,
795    context: &str,
796    suggestion: Option<&str>,
797) -> Diagnostic {
798    let expr_str = pretty_print_expr(expr);
799    let truncated = if expr_str.len() > 100 {
800        // Safely truncate at a character boundary
801        let mut end = 97;
802        while end > 0 && !expr_str.is_char_boundary(end) {
803            end -= 1;
804        }
805        format!("{}...", &expr_str[..end])
806    } else {
807        expr_str
808    };
809
810    let mut diag = Diagnostic::error(format!("{}: {}", error_type, context))
811        .with_related(format!("In expression: {}", truncated), None);
812
813    if let Some(sugg) = suggestion {
814        diag = diag.with_help(sugg.to_string());
815    }
816
817    diag
818}
819
820#[cfg(test)]
821mod tests {
822    use super::*;
823    use tensorlogic_ir::SourceLocation;
824
825    #[test]
826    fn test_diagnostic_creation() {
827        let diag = Diagnostic::error("Test error")
828            .with_help("Fix it like this")
829            .with_related("Related info", None);
830
831        assert_eq!(diag.level, DiagnosticLevel::Error);
832        assert_eq!(diag.message, "Test error");
833        assert!(diag.help.is_some());
834        assert_eq!(diag.related.len(), 1);
835    }
836
837    #[test]
838    fn test_diagnostic_format() {
839        let diag = Diagnostic::error("Test error").with_help("Fix it");
840        let formatted = diag.format();
841
842        assert!(formatted.contains("error"));
843        assert!(formatted.contains("Test error"));
844        assert!(formatted.contains("help"));
845    }
846
847    #[test]
848    fn test_diagnostic_with_span() {
849        let span = SourceSpan::single(SourceLocation::new("test.tl", 10, 5));
850        let diag = Diagnostic::error("Test error").with_span(span);
851
852        let formatted = diag.format();
853        assert!(formatted.contains("test.tl"));
854        assert!(formatted.contains("10"));
855    }
856
857    #[test]
858    fn test_diagnostic_builder() {
859        let mut builder = DiagnosticBuilder::new();
860
861        builder.add(Diagnostic::error("Error 1"));
862        builder.add(Diagnostic::warning("Warning 1"));
863        builder.add(Diagnostic::error("Error 2"));
864
865        assert!(builder.has_errors());
866        assert_eq!(builder.error_count(), 2);
867        assert_eq!(builder.diagnostics().len(), 3);
868    }
869
870    #[test]
871    fn test_diagnose_unbound_variable() {
872        let expr = TLExpr::pred("p", vec![Term::var("x")]);
873
874        let diagnostics = diagnose_expression(&expr);
875        assert_eq!(diagnostics.len(), 1);
876        assert_eq!(diagnostics[0].level, DiagnosticLevel::Error);
877        assert!(diagnostics[0].message.contains("Unbound"));
878    }
879
880    #[test]
881    fn test_diagnose_unused_binding() {
882        let expr = TLExpr::exists(
883            "x",
884            "Domain",
885            TLExpr::pred("p", vec![Term::var("y")]), // x is bound but y is used
886        );
887
888        let diagnostics = diagnose_expression(&expr);
889        // Should have warnings about unused binding
890        let warnings: Vec<_> = diagnostics
891            .iter()
892            .filter(|d| d.level == DiagnosticLevel::Warning)
893            .collect();
894        assert!(!warnings.is_empty());
895    }
896
897    #[test]
898    fn test_enhance_arity_error() {
899        let error = IrError::ArityMismatch {
900            name: "knows".to_string(),
901            expected: 2,
902            actual: 1,
903        };
904
905        let diag = enhance_error(error);
906        assert_eq!(diag.level, DiagnosticLevel::Error);
907        assert!(diag.message.contains("arity mismatch"));
908        assert!(diag.help.is_some());
909    }
910
911    #[test]
912    fn test_enhance_type_error() {
913        let error = IrError::TypeMismatch {
914            name: "knows".to_string(),
915            arg_index: 1,
916            expected: "Person".to_string(),
917            actual: "Thing".to_string(),
918        };
919
920        let diag = enhance_error(error);
921        assert!(diag.message.contains("Type mismatch"));
922        assert!(diag.help.is_some());
923    }
924
925    #[test]
926    fn test_pretty_print_predicate() {
927        let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
928        let pretty = pretty_print_expr(&expr);
929        assert_eq!(pretty, "knows(x, y)");
930    }
931
932    #[test]
933    fn test_pretty_print_quantifier() {
934        let expr = TLExpr::exists(
935            "x",
936            "Person",
937            TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
938        );
939        let pretty = pretty_print_expr(&expr);
940        assert!(pretty.contains("∃x:Person"));
941        assert!(pretty.contains("knows(x, y)"));
942    }
943
944    #[test]
945    fn test_pretty_print_complex() {
946        let expr = TLExpr::and(
947            TLExpr::pred("p", vec![Term::var("x")]),
948            TLExpr::negate(TLExpr::pred("q", vec![Term::var("y")])),
949        );
950        let pretty = pretty_print_expr(&expr);
951        assert!(pretty.contains("∧"));
952        assert!(pretty.contains("¬"));
953    }
954
955    #[test]
956    fn test_pretty_print_arithmetic() {
957        let expr = TLExpr::add(
958            TLExpr::pred("x", vec![]),
959            TLExpr::mul(TLExpr::pred("y", vec![]), TLExpr::constant(2.0)),
960        );
961        let pretty = pretty_print_expr(&expr);
962        assert!(pretty.contains("+"));
963        assert!(pretty.contains("*"));
964        assert!(pretty.contains("2"));
965    }
966
967    #[test]
968    fn test_create_detailed_error() {
969        let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
970        let diag = create_detailed_error(
971            "Compilation error",
972            &expr,
973            "Variable x is unbound",
974            Some("Add a quantifier: ∃x. <expr>"),
975        );
976
977        assert_eq!(diag.level, DiagnosticLevel::Error);
978        assert!(diag.message.contains("Compilation error"));
979        assert!(!diag.related.is_empty());
980        assert!(diag.help.is_some());
981    }
982
983    #[test]
984    fn test_pretty_print_truncation() {
985        // Create a very long expression
986        let mut expr = TLExpr::pred("p", vec![Term::var("x")]);
987        for _ in 0..10 {
988            expr = TLExpr::and(expr.clone(), TLExpr::pred("q", vec![Term::var("y")]));
989        }
990
991        let diag = create_detailed_error("Test", &expr, "context", None);
992        // Should truncate if too long
993        let related_msg = &diag.related[0].0;
994        assert!(related_msg.len() < 200); // Reasonable length
995    }
996}