tensorlogic_compiler/passes/validation.rs
1//! Validation passes for TLExpr.
2//!
3//! This module provides comprehensive validation for logical expressions before compilation.
4//! Validation helps catch errors early and provide helpful error messages.
5
6use anyhow::{anyhow, Result};
7use tensorlogic_ir::{PredicateSignature, TLExpr};
8
9use super::diagnostics::{diagnose_expression, DiagnosticLevel};
10use super::scope_analysis::{analyze_scopes, suggest_quantifiers};
11use super::type_checking::TypeChecker;
12
13/// Validate that all predicates with the same name have the same arity.
14///
15/// This is a basic validation that checks for consistency in predicate usage.
16/// Predicates must have the same number of arguments everywhere they appear.
17///
18/// # Errors
19///
20/// Returns an error if any predicate is used with different arities.
21///
22/// # Examples
23///
24/// ```
25/// use tensorlogic_compiler::passes::validate_arity;
26/// use tensorlogic_ir::{TLExpr, Term};
27///
28/// // Valid: knows/2 used consistently
29/// let expr = TLExpr::and(
30/// TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
31/// TLExpr::pred("knows", vec![Term::var("y"), Term::var("z")]),
32/// );
33/// assert!(validate_arity(&expr).is_ok());
34/// ```
35pub fn validate_arity(expr: &TLExpr) -> Result<()> {
36 expr.validate_arity().map_err(|e| anyhow!("{}", e))
37}
38
39/// Result of pre-compilation validation.
40///
41/// Contains all validation errors, warnings, and suggestions found during validation.
42#[derive(Debug, Clone)]
43pub struct ValidationResult {
44 /// Whether validation passed (no errors)
45 pub passed: bool,
46 /// Number of errors found
47 pub error_count: usize,
48 /// Number of warnings found
49 pub warning_count: usize,
50 /// All diagnostic messages (errors, warnings, hints)
51 pub diagnostics: Vec<String>,
52}
53
54impl ValidationResult {
55 /// Returns true if validation passed (no errors)
56 pub fn is_ok(&self) -> bool {
57 self.passed
58 }
59
60 /// Returns true if there are any errors
61 pub fn has_errors(&self) -> bool {
62 self.error_count > 0
63 }
64
65 /// Returns a formatted error message with all diagnostics
66 pub fn error_message(&self) -> String {
67 self.diagnostics.join("\n")
68 }
69}
70
71/// Performs comprehensive pre-compilation validation.
72///
73/// This function runs all available validation passes:
74/// 1. Arity validation (predicate consistency)
75/// 2. Scope analysis (unbound variables)
76/// 3. Enhanced diagnostics (unused bindings, type conflicts)
77///
78/// # Arguments
79///
80/// * `expr` - The expression to validate
81///
82/// # Returns
83///
84/// A `ValidationResult` containing all errors, warnings, and suggestions.
85///
86/// # Examples
87///
88/// ```
89/// use tensorlogic_compiler::passes::validate_expression;
90/// use tensorlogic_ir::{TLExpr, Term};
91///
92/// // Valid expression (fully quantified)
93/// let expr = TLExpr::exists(
94/// "x",
95/// "Person",
96/// TLExpr::exists(
97/// "y",
98/// "Person",
99/// TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
100/// ),
101/// );
102///
103/// let result = validate_expression(&expr);
104/// assert!(result.is_ok());
105/// ```
106///
107/// ```
108/// use tensorlogic_compiler::passes::validate_expression;
109/// use tensorlogic_ir::{TLExpr, Term};
110///
111/// // Expression with unbound variables
112/// let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
113///
114/// let result = validate_expression(&expr);
115/// assert!(result.has_errors());
116/// assert!(result.error_count >= 2); // x and y unbound
117/// ```
118pub fn validate_expression(expr: &TLExpr) -> ValidationResult {
119 let mut diagnostics = Vec::new();
120 let mut error_count = 0;
121 let mut warning_count = 0;
122
123 // 1. Arity validation
124 if let Err(e) = validate_arity(expr) {
125 diagnostics.push(format!("Arity error: {}", e));
126 error_count += 1;
127 }
128
129 // 2. Scope analysis
130 match analyze_scopes(expr) {
131 Ok(scope_result) => {
132 // Check for unbound variables
133 if !scope_result.unbound_variables.is_empty() {
134 for var in &scope_result.unbound_variables {
135 diagnostics.push(format!("Unbound variable: '{}'", var));
136 error_count += 1;
137 }
138
139 // Provide helpful suggestion
140 if let Ok(suggestions) = suggest_quantifiers(expr) {
141 if !suggestions.is_empty() {
142 diagnostics.push(format!("Suggestion: {}", suggestions.join(", ")));
143 }
144 }
145 }
146
147 // Check for type conflicts
148 for conflict in &scope_result.type_conflicts {
149 diagnostics.push(format!(
150 "Type conflict: variable '{}' has conflicting types '{}' and '{}'",
151 conflict.variable, conflict.type1, conflict.type2
152 ));
153 error_count += 1;
154 }
155 }
156 Err(e) => {
157 diagnostics.push(format!("Scope analysis error: {}", e));
158 error_count += 1;
159 }
160 }
161
162 // 3. Enhanced diagnostics (warnings and hints)
163 let diag_messages = diagnose_expression(expr);
164 for diag in diag_messages {
165 let formatted = diag.format();
166 match diag.level {
167 DiagnosticLevel::Error => {
168 // Skip if we already reported this error above
169 if !diagnostics.iter().any(|d| d.contains(&diag.message)) {
170 diagnostics.push(formatted);
171 error_count += 1;
172 }
173 }
174 DiagnosticLevel::Warning => {
175 diagnostics.push(formatted);
176 warning_count += 1;
177 }
178 DiagnosticLevel::Info | DiagnosticLevel::Hint => {
179 diagnostics.push(formatted);
180 }
181 }
182 }
183
184 ValidationResult {
185 passed: error_count == 0,
186 error_count,
187 warning_count,
188 diagnostics,
189 }
190}
191
192/// Validates an expression with type signatures.
193///
194/// This is an extended validation that includes type checking against
195/// registered predicate signatures.
196///
197/// # Arguments
198///
199/// * `expr` - The expression to validate
200/// * `signatures` - Predicate signatures for type checking
201///
202/// # Returns
203///
204/// A `ValidationResult` with type checking errors included.
205///
206/// # Examples
207///
208/// ```
209/// use tensorlogic_compiler::passes::validate_expression_with_types;
210/// use tensorlogic_ir::{PredicateSignature, TLExpr, Term, TypeAnnotation};
211///
212/// let signatures = vec![
213/// PredicateSignature {
214/// name: "knows".to_string(),
215/// arity: 2,
216/// arg_types: vec![
217/// TypeAnnotation { type_name: "Person".to_string() },
218/// TypeAnnotation { type_name: "Person".to_string() },
219/// ],
220/// }
221/// ];
222///
223/// // Fully quantified expression
224/// let expr = TLExpr::exists(
225/// "x",
226/// "Person",
227/// TLExpr::exists(
228/// "y",
229/// "Person",
230/// TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
231/// ),
232/// );
233///
234/// let result = validate_expression_with_types(&expr, &signatures);
235/// assert!(result.is_ok());
236/// ```
237pub fn validate_expression_with_types(
238 expr: &TLExpr,
239 signatures: &[PredicateSignature],
240) -> ValidationResult {
241 let mut result = validate_expression(expr);
242
243 // Add type checking
244 use tensorlogic_ir::SignatureRegistry;
245 let mut registry = SignatureRegistry::new();
246 for sig in signatures {
247 registry.register(sig.clone());
248 }
249
250 let checker = TypeChecker::new(registry);
251 if let Err(e) = checker.check_expr(expr) {
252 result.diagnostics.push(format!("Type error: {}", e));
253 result.error_count += 1;
254 result.passed = false;
255 }
256
257 result
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use tensorlogic_ir::Term;
264
265 #[test]
266 fn test_validate_expression_ok() {
267 // Fully quantified expression with no unbound variables
268 let expr = TLExpr::exists(
269 "x",
270 "Person",
271 TLExpr::exists(
272 "y",
273 "Person",
274 TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
275 ),
276 );
277
278 let result = validate_expression(&expr);
279 if !result.is_ok() {
280 eprintln!("Validation failed with errors:");
281 for diag in &result.diagnostics {
282 eprintln!(" - {}", diag);
283 }
284 }
285 assert!(result.is_ok());
286 assert_eq!(result.error_count, 0);
287 }
288
289 #[test]
290 fn test_validate_expression_partial_binding() {
291 // Expression where y is bound but x is not
292 let expr = TLExpr::exists(
293 "y",
294 "Person",
295 TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
296 );
297
298 let result = validate_expression(&expr);
299 eprintln!(
300 "Error count: {}, diagnostics: {:?}",
301 result.error_count, result.diagnostics
302 );
303 assert!(result.has_errors());
304 // Expected: 1 error for unbound x, but diagnose_expression also reports it
305 // So we get 2 total (1 from scope analysis, 1 from diagnostics module)
306 assert!(result.error_count >= 1); // At least 1 for unbound x
307 }
308
309 #[test]
310 fn test_validate_expression_unbound_vars() {
311 let expr = TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]);
312
313 let result = validate_expression(&expr);
314 assert!(result.has_errors());
315 // Both x and y are unbound - at least 2 errors
316 assert!(result.error_count >= 2);
317 }
318
319 #[test]
320 fn test_validate_expression_arity_mismatch() {
321 let expr = TLExpr::and(
322 TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
323 TLExpr::pred("knows", vec![Term::var("z")]),
324 );
325
326 let result = validate_expression(&expr);
327 assert!(result.has_errors());
328 assert!(result.diagnostics.iter().any(|d| d.contains("Arity")));
329 }
330
331 #[test]
332 fn test_validate_expression_with_warnings() {
333 // Expression with unused binding
334 let expr = TLExpr::exists(
335 "x",
336 "Person",
337 TLExpr::pred("p", vec![Term::var("y")]), // x not used
338 );
339
340 let result = validate_expression(&expr);
341 assert!(result.warning_count > 0);
342 }
343
344 #[test]
345 fn test_validate_with_types() {
346 use tensorlogic_ir::TypeAnnotation;
347
348 let signatures = vec![PredicateSignature {
349 name: "knows".to_string(),
350 arity: 2,
351 arg_types: vec![
352 TypeAnnotation {
353 type_name: "Person".to_string(),
354 },
355 TypeAnnotation {
356 type_name: "Person".to_string(),
357 },
358 ],
359 }];
360
361 // Fully quantified expression
362 let expr = TLExpr::exists(
363 "x",
364 "Person",
365 TLExpr::exists(
366 "y",
367 "Person",
368 TLExpr::pred("knows", vec![Term::var("x"), Term::var("y")]),
369 ),
370 );
371
372 let result = validate_expression_with_types(&expr, &signatures);
373 assert!(result.is_ok());
374 }
375
376 #[test]
377 fn test_validation_result_message() {
378 let expr = TLExpr::pred("knows", vec![Term::var("x")]);
379
380 let result = validate_expression(&expr);
381 let message = result.error_message();
382 assert!(!message.is_empty());
383 assert!(message.contains("Unbound"));
384 }
385}