ruchy 4.2.0

A systems scripting language that transpiles to idiomatic Rust with extreme quality engineering
Documentation
// RUNTIME-002: Implement Structs (Value Types) - EXTREME TDD
// RED → GREEN → REFACTOR cycle
//
// This test file follows EXTREME TDD methodology:
// 1. RED: Write tests FIRST (all marked #[ignore], they WILL fail)
// 2. GREEN: Implement minimal code to make tests pass
// 3. REFACTOR: Add property tests, mutation tests, optimize
//
// Requirements from roadmap.yaml:
// - Value::Struct(HashMap<String, Value>) runtime representation

#![allow(clippy::ignore_without_reason)] // TDD RED phase - tests intentionally ignored until impl
#![allow(missing_docs)]
// - Struct instantiation: Point { x: 1.0, y: 2.0 }
// - Field access: point.x, point.y
// - Value semantics (copy on assignment)
// - Error handling for missing/invalid fields

use assert_cmd::Command;
use predicates::prelude::*;

/// Helper to get ruchy binary command
fn ruchy_cmd() -> Command {
    assert_cmd::cargo::cargo_bin_cmd!("ruchy")
}

// ==================== RED PHASE: Unit Tests (Will Fail Initially) ====================

/// Test 1: Basic struct instantiation
#[test]
fn test_runtime_002_struct_instantiation_basic() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10, y: 20 }; p.x")
        .assert()
        .success()
        .stdout(predicate::str::contains("10"));
}

/// Test 2: Struct field access (x field)
#[test]
fn test_runtime_002_struct_field_access_x() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10, y: 20 }; p.x")
        .assert()
        .success()
        .stdout(predicate::str::contains("10"));
}

/// Test 3: Struct field access (y field)
#[test]
fn test_runtime_002_struct_field_access_y() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10, y: 20 }; p.y")
        .assert()
        .success()
        .stdout(predicate::str::contains("20"));
}

/// Test 4: Struct value semantics (copy on assignment)
#[test]
fn test_runtime_002_struct_value_semantics_copy() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p1 = Point { x: 0, y: 0 }; let p2 = p1; p2.x = 10; println!(p1.x)")
        .assert()
        .success()
        .stdout(predicate::str::contains("0")); // p1.x unchanged (value semantics)
}

/// Test 5: Nested struct instantiation
#[test]
fn test_runtime_002_struct_nested_structs() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32 }; struct Rectangle { top_left: Point }; let rect = Rectangle { top_left: Point { x: 10 } }; rect.top_left.x")
        .assert()
        .success()
        .stdout(predicate::str::contains("10"));
}

/// Test 6: Struct with different field types
#[test]
fn test_runtime_002_struct_mixed_field_types() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Person { name: String, age: i32 }; let p = Person { name: \"Alice\", age: 30 }; p.name")
        .assert()
        .success()
        .stdout(predicate::str::contains("Alice"));
}

/// Test 7: Error handling - missing field
#[test]
fn test_runtime_002_struct_error_missing_field() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10 }") // Missing y field
        .assert()
        .failure() // Should fail with error
        .stderr(predicate::str::contains("missing field").or(predicate::str::contains("field")));
}

/// Test 8: Error handling - extra field
#[test]
fn test_runtime_002_struct_error_extra_field() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10, y: 20, z: 30 }") // Extra z field
        .assert()
        .failure() // Should fail with error
        .stderr(predicate::str::contains("unknown field").or(predicate::str::contains("field")));
}

/// Test 9: Error handling - accessing non-existent field
#[test]
fn test_runtime_002_struct_error_invalid_field_access() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: i32, y: i32 }; let p = Point { x: 10, y: 20 }; p.z") // z doesn't exist
        .assert()
        .failure() // Should fail with error
        .stderr(predicate::str::contains("field").or(predicate::str::contains("not found")));
}

/// Test 10: Struct with float fields
#[test]
fn test_runtime_002_struct_float_fields() {
    ruchy_cmd()
        .arg("-e")
        .arg("struct Point { x: f64, y: f64 }; let p = Point { x: 1.5, y: 2.5 }; p.x")
        .assert()
        .success()
        .stdout(predicate::str::contains("1.5"));
}

// ==================== REFACTOR PHASE: Property Tests (10K+ Iterations) ====================

#[cfg(test)]
mod property_tests {
    use super::*;
    use proptest::prelude::*;

    /// Property test: Struct field access always returns the correct value
    /// Validates: Field access preserves values across any valid integers
    #[test]
    fn prop_struct_field_access_preserves_values() {
        proptest!(|(x: i32, y: i32)| {
            let code = format!(
                "struct Point {{ x: i32, y: i32 }}; let p = Point {{ x: {x}, y: {y} }}; p.x"
            );
            let result = ruchy_cmd()
                .arg("-e")
                .arg(&code)
                .assert()
                .success();

            let stdout = String::from_utf8_lossy(&result.get_output().stdout);
            prop_assert!(stdout.contains(&x.to_string()),
                "Expected stdout to contain {}, got: {}", x, stdout);
        });
    }

    /// Property test: Nested structs maintain correct values
    /// Validates: Nested struct instantiation and field access work for any integers
    #[test]
    fn prop_nested_structs_preserve_values() {
        proptest!(|(x: i32)| {
            let code = format!(
                "struct Point {{ x: i32 }}; struct Rectangle {{ top_left: Point }}; \
                 let rect = Rectangle {{ top_left: Point {{ x: {x} }} }}; rect.top_left.x"
            );
            let result = ruchy_cmd()
                .arg("-e")
                .arg(&code)
                .assert()
                .success();

            let stdout = String::from_utf8_lossy(&result.get_output().stdout);
            prop_assert!(stdout.contains(&x.to_string()),
                "Expected stdout to contain {}, got: {}", x, stdout);
        });
    }

    /// Property test: Missing required field always produces error
    /// Validates: Error handling is consistent across all field names
    #[test]
    fn prop_missing_field_always_errors() {
        proptest!(|(x: i32)| {
            let code = format!(
                "struct Point {{ x: i32, y: i32 }}; let p = Point {{ x: {x} }}"
            );
            ruchy_cmd()
                .arg("-e")
                .arg(&code)
                .assert()
                .failure()
                .stderr(predicate::str::contains("field")
                    .or(predicate::str::contains("Missing")));
        });
    }

    /// Property test: Invalid field access always produces error
    /// Validates: Error handling works for any struct instance
    #[test]
    fn prop_invalid_field_access_always_errors() {
        proptest!(|(x: i32, y: i32)| {
            let code = format!(
                "struct Point {{ x: i32, y: i32 }}; let p = Point {{ x: {x}, y: {y} }}; p.z"
            );
            ruchy_cmd()
                .arg("-e")
                .arg(&code)
                .assert()
                .failure()
                .stderr(predicate::str::contains("field")
                    .or(predicate::str::contains("not found")));
        });
    }

    /// Property test: Float fields work for any valid f64 values
    /// Validates: Struct field access works with floating point numbers
    #[test]
    fn prop_float_fields_work() {
        proptest!(|(x in -1000.0f64..1000.0f64, y in -1000.0f64..1000.0f64)| {
            let code = format!(
                "struct Point {{ x: f64, y: f64 }}; let p = Point {{ x: {x}, y: {y} }}; p.x"
            );
            let result = ruchy_cmd()
                .arg("-e")
                .arg(&code)
                .assert()
                .success();

            let stdout = String::from_utf8_lossy(&result.get_output().stdout);
            // Float comparison is tricky, just verify it's a number
            prop_assert!(stdout.trim().parse::<f64>().is_ok(),
                "Expected stdout to contain a float, got: {}", stdout);
        });
    }
}

// ==================== RED PHASE: Mutation Test Targets ====================

// Mutation testing targets (for REFACTOR phase):
// 1. Field access logic (wrong field name handling)
// 2. Type checking in struct instantiation
// 3. Copy semantics (ensure no reference sharing)
// 4. Error handling branches (missing/extra fields)

// ==================== Test Summary ====================

#[test]
fn test_runtime_002_red_phase_summary() {
    // This test documents the RED phase test plan
    //
    // Unit Tests Created: 10
    // 1. test_runtime_002_struct_instantiation_basic
    // 2. test_runtime_002_struct_field_access_x
    // 3. test_runtime_002_struct_field_access_y
    // 4. test_runtime_002_struct_value_semantics_copy
    // 5. test_runtime_002_struct_nested_structs
    // 6. test_runtime_002_struct_mixed_field_types
    // 7. test_runtime_002_struct_error_missing_field
    // 8. test_runtime_002_struct_error_extra_field
    // 9. test_runtime_002_struct_error_invalid_field_access
    // 10. test_runtime_002_struct_float_fields
    //
    // All tests currently #[ignore]d and will FAIL when un-ignored (RED phase)
    //
    // Next Step (GREEN phase):
    // 1. Add Value::Struct variant to src/runtime/interpreter.rs
    // 2. Implement struct instantiation in eval
    // 3. Implement field access in eval
    // 4. Implement value semantics (clone on assignment)
    // 5. Un-ignore tests one by one and make them pass
    //
    // After GREEN (REFACTOR phase):
    // 1. Add 10K+ property tests
    // 2. Run mutation tests (target ≥75%)
    // 3. Optimize if needed while maintaining tests

    assert!(
        true,
        "RED phase: 10 tests created, all will fail when un-ignored"
    );
}