use super::fitness::SabResult;
use super::sandbox::{Sandbox, SandboxConfig, SandboxResult};
use super::{FitnessMetrics, FitnessWeights, GenerationRating};
use std::path::{Path, PathBuf};
use std::time::Duration;
mod semaphore {
use std::sync::{Condvar, Mutex};
pub struct Semaphore {
count: Mutex<usize>,
condvar: Condvar,
}
impl Semaphore {
pub fn new(count: usize) -> Self {
Self {
count: Mutex::new(count),
condvar: Condvar::new(),
}
}
pub fn acquire(&self) {
let mut count = self.count.lock().unwrap();
while *count == 0 {
count = self.condvar.wait(count).unwrap();
}
*count -= 1;
}
pub fn release(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
self.condvar.notify_one();
}
}
}
#[derive(Debug, Clone)]
pub struct Hypothesis {
pub id: String,
pub description: String,
pub patch: String,
pub target_files: Vec<PathBuf>,
pub property_test: Option<String>,
}
#[derive(Debug)]
pub struct HypothesisResult {
pub id: String,
pub description: String,
pub compiled: bool,
pub sandbox_result: Option<SandboxResult>,
pub sab_result: Option<SabResult>,
pub fitness: Option<FitnessMetrics>,
pub composite_score: f64,
pub rating: GenerationRating,
pub patch: String,
}
#[derive(Debug, Clone)]
pub struct TournamentConfig {
pub max_parallel: usize,
pub timeout: Duration,
pub weights: FitnessWeights,
pub sandbox: SandboxConfig,
}
impl Default for TournamentConfig {
fn default() -> Self {
Self {
max_parallel: 4,
timeout: Duration::from_secs(3600),
weights: FitnessWeights::default(),
sandbox: SandboxConfig::default(),
}
}
}
pub fn run_tournament(
hypotheses: Vec<Hypothesis>,
config: &TournamentConfig,
repo_root: &Path,
) -> Vec<HypothesisResult> {
use std::sync::{Arc, Mutex};
use std::thread;
let results = Arc::new(Mutex::new(Vec::new()));
let semaphore = Arc::new(semaphore::Semaphore::new(config.max_parallel));
let handles: Vec<_> = hypotheses
.into_iter()
.map(|h| {
let sem = semaphore.clone();
let res = results.clone();
let cfg = config.clone();
let root = repo_root.to_path_buf();
thread::spawn(move || {
sem.acquire();
let result = evaluate_hypothesis(h, &cfg, &root);
sem.release();
res.lock().unwrap().push(result);
})
})
.collect();
for h in handles {
let _ = h.join();
}
let mut results = Arc::try_unwrap(results)
.unwrap_or_else(|_| panic!("Failed to unwrap results"))
.into_inner()
.unwrap();
results.sort_by(|a, b| {
b.composite_score
.partial_cmp(&a.composite_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results
}
fn evaluate_hypothesis(
hypothesis: Hypothesis,
config: &TournamentConfig,
repo_root: &Path,
) -> HypothesisResult {
let sandbox = match Sandbox::create(&hypothesis.id, repo_root, config.sandbox.clone()) {
Ok(s) => s,
Err(_e) => {
return HypothesisResult {
id: hypothesis.id,
description: hypothesis.description,
compiled: false,
sandbox_result: None,
sab_result: None,
fitness: None,
composite_score: 0.0,
rating: GenerationRating::Frost,
patch: hypothesis.patch,
};
}
};
if !sandbox.apply_patch(&hypothesis.patch).unwrap_or(false) {
let _ = sandbox.destroy();
return HypothesisResult {
id: hypothesis.id,
description: hypothesis.description,
compiled: false,
sandbox_result: None,
sab_result: None,
fitness: None,
composite_score: 0.0,
rating: GenerationRating::Frost,
patch: hypothesis.patch,
};
}
let sandbox_result = match sandbox.evaluate() {
Ok(r) => r,
Err(_) => {
let _ = sandbox.destroy();
return HypothesisResult {
id: hypothesis.id,
description: hypothesis.description,
compiled: false,
sandbox_result: None,
sab_result: None,
fitness: None,
composite_score: 0.0,
rating: GenerationRating::Frost,
patch: hypothesis.patch,
};
}
};
let compiled = sandbox_result.compiled;
let tests_passed = sandbox_result.tests_passed;
let tests_total = sandbox_result.tests_total;
let compile_duration = sandbox_result.compile_duration;
let peak_memory_bytes = sandbox_result.peak_memory_bytes;
let test_duration = sandbox_result.test_duration;
let _ = sandbox.destroy();
let timeout_secs = config.timeout.as_secs() as f64;
let wall_clock_secs = (compile_duration + test_duration).as_secs_f64();
let test_coverage_pct = if tests_total > 0 {
(tests_passed as f64 / tests_total as f64) * 100.0
} else {
0.0
};
let sab_score = if compiled { test_coverage_pct } else { 0.0 };
let fitness = FitnessMetrics {
sab_score,
tokens_used: 0,
token_budget: 0,
wall_clock_secs,
timeout_secs,
test_coverage_pct,
binary_size_mb: (peak_memory_bytes as f64) / (1024.0 * 1024.0),
max_binary_size_mb: 1024.0,
tests_passed,
tests_total,
visual_score: 0.0,
};
let fitness_composite = config.weights.composite(&fitness);
let rating = if !compiled {
GenerationRating::Frost
} else if tests_passed == tests_total && tests_total > 0 {
GenerationRating::Bloom
} else if tests_passed as f64 / tests_total.max(1) as f64 > 0.95 {
GenerationRating::Grow
} else if tests_passed as f64 / tests_total.max(1) as f64 > 0.50 {
GenerationRating::Wilt
} else {
GenerationRating::Frost
};
let composite = if compiled {
fitness_composite.max((tests_passed as f64 / tests_total.max(1) as f64) * 100.0)
} else {
0.0
};
HypothesisResult {
id: hypothesis.id,
description: hypothesis.description,
compiled,
sandbox_result: Some(sandbox_result),
sab_result: None, fitness: Some(fitness),
composite_score: composite,
rating,
patch: hypothesis.patch,
}
}
#[cfg(test)]
#[path = "../../tests/unit/evolution/tournament/tournament_test.rs"]
mod tests;