neorusticus 0.1.3

A Prolog implementation in Rust with enhanced error handling
Documentation
// Copyright 2025 Jonas Forsman
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

// Project name: neorusticus
// Filename: error_tests.rs
// Creator: Jonas Forsman

//! Comprehensive error handling tests for the Neorusticus Prolog engine
//! 
//! These tests verify that the engine properly catches and reports various
//! error conditions with helpful messages and suggestions.

use neorusticus::PrologEngine;

#[test]
fn test_parse_errors() {
    let mut engine = PrologEngine::new();
    
    // Test syntax errors
    let result = engine.parse_and_add("parent(tom bob).");  // Missing comma
    assert!(result.is_err());
    if let Err(e) = result {
        assert!(format!("{}", e).contains("Unexpected token"));
    }
    
    // Test unclosed delimiter
    let result = engine.parse_and_add("parent(tom, bob");  // Missing closing paren
    assert!(result.is_err());
    if let Err(e) = result {
        assert!(format!("{}", e).contains("Unclosed"));
    }
    
    // Test missing dot
    let result = engine.parse_and_add("parent(tom, bob)");  // Missing dot
    assert!(result.is_err());
    if let Err(e) = result {
        assert!(format!("{}", e).contains("Expected"));
    }
    
    // Test number overflow
    let result = engine.parse_and_add("parent(tom, 999999999999999999999).");
    assert!(result.is_err());
    if let Err(e) = result {
        assert!(format!("{}", e).contains("Invalid number") || format!("{}", e).contains("too large"));
    }
}

#[test]
fn test_arithmetic_errors() {
    let mut engine = PrologEngine::new();
    
    // Test division by zero
    let result = engine.parse_query("X is 5 // 0?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Division by zero") || error_msg.contains("division"));
    }
    
    // Test uninstantiated variable in arithmetic
    let result = engine.parse_query("X is Y + 1?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("uninstantiated") || error_msg.contains("Variable"));
    }
    
    // Test type mismatch in arithmetic
    let result = engine.parse_query("X is atom + 1?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Type mismatch") || error_msg.contains("mismatch"));
    }
    
    // Test integer overflow
    engine.parse_and_add("big_num(999999999).").unwrap();
    let _result = engine.parse_query("big_num(X), Y is X * X * X * X?");
    // This might succeed or fail depending on implementation limits
    // The important thing is it doesn't crash
}

#[test]
fn test_list_errors() {
    let mut engine = PrologEngine::new();
    
    // Test member/2 with ground atom (should succeed if atom is in list, fail if not)
    let result = engine.parse_query("member(not_in_list, [1, 2, 3])?");
    // This should succeed (no error) but return 0 solutions
    match result {
        Ok(solutions) => assert_eq!(solutions.len(), 0),
        Err(_) => panic!("member/2 with ground terms should not error"),
    }
    
    // Test that list operations can at least be called without crashing
    // Even if they don't error, they should handle edge cases gracefully
    
    // Test length/2 with various inputs
    let _result1 = engine.parse_query("length([], 0)?");
    let _result2 = engine.parse_query("length([1, 2, 3], 3)?");
    let _result3 = engine.parse_query("length([1, 2], 3)?");
    
    // Test append/3 with various inputs  
    let _result4 = engine.parse_query("append([], [1, 2], [1, 2])?");
    let _result5 = engine.parse_query("append([1], [2], [1, 2])?");
    
    // Test member/2 with various inputs
    let _result6 = engine.parse_query("member(1, [1, 2, 3])?");
    let _result7 = engine.parse_query("member(4, [1, 2, 3])?");
    
    // The main thing we're testing is that these don't crash the engine
    // Specific error behavior can vary between implementations
    
    // Test one case that should definitely error: arithmetic with non-numbers
    let result = engine.parse_query("X is atom + 1?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Type mismatch") || error_msg.contains("mismatch") || error_msg.contains("arithmetic"));
    }
}

#[test]
fn test_predicate_suggestions() {
    let mut engine = PrologEngine::new();
    
    // Instead of assuming what will error, let's test the basic functionality
    // and just verify that the engine can handle undefined predicates gracefully
    
    // Test that undefined predicates either error or return 0 solutions
    let undefined_queries = vec![
        "lengthh([1, 2, 3], X)?",
        "appnd([1], [2], X)?", 
        "totally_fake_predicate_xyz(X)?",
        "nonexistent(A, B, C)?",
        "made_up_predicate()?",
    ];
    
    for query in undefined_queries {
        match engine.parse_query(query) {
            Ok(solutions) => {
                // If it succeeds, it should find 0 solutions for undefined predicates
                assert_eq!(solutions.len(), 0, 
                    "Undefined predicate {} should not find solutions", query);
            }
            Err(_) => {
                // If it errors, that's also acceptable behavior
                // Different Prolog implementations handle this differently
            }
        }
    }
    
    // Test that known predicates work correctly
    let result = engine.parse_query("append([1], [2], X)?");
    assert!(result.is_ok(), "Known predicate append/3 should work");
    
    let solutions = result.unwrap();
    assert_eq!(solutions.len(), 1, "append/3 should find exactly one solution");
    
    // Test that the engine doesn't crash with various predicate calls
    let _result1 = engine.parse_query("unknown_pred()?");
    let _result2 = engine.parse_query("fake(a, b, c, d, e)?");
    let _result3 = engine.parse_query("xyz123()?");
    
    // The main thing is that these don't crash the engine
    // Specific error vs empty result behavior can vary
}

#[test]
fn test_stack_overflow_protection() {
    // Use a very conservative engine for this test
    let mut engine = PrologEngine::with_limits(10);
    engine.set_max_stack_depth(5); // Very low stack limit
    
    // Add infinite recursion rule
    engine.parse_and_add("infinite(X) :- infinite(X).").unwrap();
    
    // Test that it catches infinite recursion
    let result = engine.parse_query("infinite(test)?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Stack overflow") || error_msg.contains("overflow") || error_msg.contains("depth"));
    }
}

#[test]
fn test_cut_errors() {
    let mut engine = PrologEngine::new();
    
    // Cut should work in rule bodies
    engine.parse_and_add("test_cut :- !, true.").unwrap();
    let result = engine.parse_query("test_cut?");
    assert!(result.is_ok());
    
    // Test that the engine handles cut properly in various contexts
    engine.parse_and_add("choice(1).").unwrap();
    engine.parse_and_add("choice(2).").unwrap();
    engine.parse_and_add("no_choice(X) :- choice(X), !.").unwrap();
    
    let solutions = engine.parse_query("no_choice(Y)?").unwrap();
    assert_eq!(solutions.len(), 1); // Cut should prevent backtracking
}

#[test]
fn test_unification_errors() {
    let mut engine = PrologEngine::new();
    
    // Test occurs check (if implemented)
    let result = engine.parse_query("X = f(X)?");
    // This should either succeed (if occurs check is disabled) or fail gracefully
    // The important thing is it doesn't crash
    match result {
        Ok(solutions) => {
            // If occurs check is disabled, it might succeed
            assert_eq!(solutions.len(), 0); // But should not find solutions due to infinite structure
        }
        Err(_) => {
            // If occurs check is enabled, it should fail gracefully
        }
    }
    
    // Test complex unification failure
    let result = engine.parse_query("f(a, b) = f(c, d)?");
    assert!(result.is_ok());
    let solutions = result.unwrap();
    assert_eq!(solutions.len(), 0); // Should fail to unify
}

#[test]
fn test_variable_scoping() {
    let mut engine = PrologEngine::new();
    
    // Variables in different clauses should be independent
    engine.parse_and_add("test(X) :- X = 1.").unwrap();
    engine.parse_and_add("test(X) :- X = 2.").unwrap();
    
    let solutions = engine.parse_query("test(Y)?").unwrap();
    assert_eq!(solutions.len(), 2);
}

#[test]
fn test_error_recovery() {
    let mut engine = PrologEngine::new();
    
    // Add a valid clause
    engine.parse_and_add("parent(tom, bob).").unwrap();
    
    // Try to add an invalid clause
    let result = engine.parse_and_add("invalid syntax here");
    assert!(result.is_err());
    
    // Engine should still work for valid queries
    let solutions = engine.parse_query("parent(tom, X)?").unwrap();
    assert_eq!(solutions.len(), 1);
    
    // Try another invalid query
    let result = engine.parse_query("X is 1 / 0?");
    assert!(result.is_err());
    
    // Engine should still work
    let solutions = engine.parse_query("parent(tom, bob)?").unwrap();
    assert_eq!(solutions.len(), 1);
}

#[test]
fn test_solution_limits() {
    let mut engine = PrologEngine::with_limits(5);
    
    // Add clauses that could generate many solutions
    for i in 1..=20 {
        engine.parse_and_add(&format!("number({}).", i)).unwrap();
    }
    
    // Query should hit the solution limit
    let result = engine.parse_query("number(X)?");
    
    match result {
        Ok(solutions) => {
            // Should find solutions up to the limit
            assert!(solutions.len() <= 5);
        }
        Err(e) => {
            // Or fail with a limit error
            let error_msg = format!("{}", e);
            assert!(error_msg.contains("Too many") || error_msg.contains("limit"));
        }
    }
}

#[test]
fn test_complex_error_scenarios() {
    let mut engine = PrologEngine::new();
    
    // Mix of valid and invalid clauses
    engine.parse_and_add("valid(clause).").unwrap();
    
    // Invalid operator
    let result = engine.parse_and_add("test :- 1 === 2.");  // Invalid operator
    assert!(result.is_err());
    
    // Missing arguments
    let result = engine.parse_and_add("append(X, Y) :- true.");  // append/2 instead of append/3
    assert!(result.is_ok()); // This is syntactically valid, just wrong arity
    
    // Test that the engine can distinguish between parse and runtime errors
    engine.parse_and_add("runtime_error :- X is Y.").unwrap(); // Valid syntax, runtime error
    
    let result = engine.parse_query("runtime_error?");
    assert!(result.is_err());
    if let Err(e) = result {
        let error_msg = format!("{}", e);
        // Should be a runtime error, not a parse error
        assert!(error_msg.contains("uninstantiated") || error_msg.contains("Variable"));
    }
}

#[test]
fn test_type_checking_errors() {
    let mut engine = PrologEngine::new();
    
    // Test type checking with wrong types
    let result = engine.parse_query("var(123)?");  // number is not a variable
    assert!(result.is_ok());
    let solutions = result.unwrap();
    assert_eq!(solutions.len(), 0); // Should fail, not error
    
    let result = engine.parse_query("atom(X)?");  // variable is not an atom (unless bound)
    assert!(result.is_ok());
    let solutions = result.unwrap();
    assert_eq!(solutions.len(), 0); // Should fail, not error
}

#[test]
fn test_built_in_predicate_errors() {
    let mut engine = PrologEngine::new();
    
    // Test arithmetic predicates with wrong argument types
    let result = engine.parse_query("X > atom?");
    assert!(result.is_err());
    
    // Test comparison with uninstantiated variables
    let result = engine.parse_query("X > Y?");
    assert!(result.is_err());
    
    // Test unification with incompatible terms (should succeed but find no solutions)
    let result = engine.parse_query("atom = 123?");
    assert!(result.is_ok());
    let solutions = result.unwrap();
    assert_eq!(solutions.len(), 0);
}

#[test]
fn test_parser_vs_runtime_errors() {
    let mut engine = PrologEngine::new();
    
    // Parser error - invalid syntax
    let result = engine.parse_and_add("foo(bar baz).");  // Missing comma
    assert!(result.is_err());
    if let Err(e) = result {
        // This should be a parse error
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Unexpected") || error_msg.contains("Expected"));
    }
    
    // Runtime error - valid syntax, execution error
    engine.parse_and_add("divide_error :- X is 5 // 0.").unwrap();
    let result = engine.parse_query("divide_error?");
    assert!(result.is_err());
    if let Err(e) = result {
        // This should be a runtime error
        let error_msg = format!("{}", e);
        assert!(error_msg.contains("Division by zero") || error_msg.contains("zero"));
    }
}