1use anyhow::{anyhow, Result};
4use tensorlogic_ir::{IrError, SourceSpan, TLExpr, Term};
5
6use super::scope_analysis::analyze_scopes;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DiagnosticLevel {
11 Error,
12 Warning,
13 Info,
14 Hint,
15}
16
17#[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 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
96pub 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
148pub 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
192pub fn diagnose_expression(expr: &TLExpr) -> Vec<Diagnostic> {
194 let mut diagnostics = Vec::new();
195
196 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 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 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 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 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 _ => {}
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 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 _ => 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
479pub 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 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 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 _ => "<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
791pub 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 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")]), );
887
888 let diagnostics = diagnose_expression(&expr);
889 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 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 let related_msg = &diag.related[0].0;
994 assert!(related_msg.len() < 200); }
996}