use crate::language::Language;
use crate::registry::Registry;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub concept_id: String,
pub language: Language,
pub passed: bool,
pub error: Option<String>,
pub duration_ms: u64,
}
#[must_use]
pub fn validation_command(lang: Language) -> Option<&'static str> {
match lang {
Language::Rust => Some("rustc --edition 2024 {file} -o {out} && {out}"),
Language::Python => Some("python3 -c \"exec(open('{file}').read())\""),
Language::C => Some("gcc -std=c17 -Wall -Werror {file} -o {out} -lm -lpthread && {out}"),
Language::Go => Some("go run {file}"),
Language::TypeScript => Some("npx tsx {file}"),
Language::Shell => Some("bash {file}"),
Language::Zig => Some("zig build-exe {file} -femit-bin={out} && {out}"),
Language::AsmX86_64 => {
Some("as --64 {file} -o {out}.o && ld {out}.o -o {out} && {out} ; rm -f {out}.o")
}
Language::AsmAarch64 => Some(
"aarch64-linux-gnu-as {file} -o {out}.o && aarch64-linux-gnu-ld {out}.o -o {out} && qemu-aarch64 {out} ; rm -f {out}.o",
),
#[cfg(feature = "openqasm")]
Language::OpenQASM => None, #[cfg(not(feature = "openqasm"))]
Language::OpenQASM => Some(
"QASM_PY=$(if [ -f .venv/bin/python3 ]; then echo .venv/bin/python3; else echo python3; fi) && $QASM_PY -c \"from qiskit import qasm2; import os; qc = qasm2.load('{file}', include_path=[os.path.dirname('{file}') + '/..']); print(f'valid: {{qc.num_qubits}}q depth={{qc.depth()}}')\"",
),
Language::Cyrius => {
Some(
"CYR_HOME=${CYRIUS_HOME:-$HOME/Repos/cyrius} && CYR_FILE=$(realpath {file}) && cd \"$CYR_HOME\" && cat \"$CYR_FILE\" | ./build/cc2 > {out} && chmod +x {out} && {out}",
)
}
}
}
#[cfg(feature = "openqasm")]
fn validate_qasm_native(concept_id: &str, file_path: &Path) -> ValidationResult {
use openqasm::{GenericError, Parser, SourceCache};
let start = Instant::now();
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
return ValidationResult {
concept_id: concept_id.to_string(),
language: Language::OpenQASM,
passed: false,
error: Some(format!("failed to read file: {e}")),
duration_ms: 0,
};
}
};
let include_dir = file_path.parent().and_then(|p| p.parent());
let mut cache = SourceCache::new();
let mut parser = Parser::new(&mut cache);
parser.parse_source(source, include_dir);
let duration_ms = start.elapsed().as_millis() as u64;
match parser.done().to_errors() {
Ok(_prog) => ValidationResult {
concept_id: concept_id.to_string(),
language: Language::OpenQASM,
passed: true,
error: None,
duration_ms,
},
Err(errors) => ValidationResult {
concept_id: concept_id.to_string(),
language: Language::OpenQASM,
passed: false,
error: Some(format!("{} parse errors", errors.errors.len())),
duration_ms,
},
}
}
pub fn run_validation(concept_id: &str, language: Language, file_path: &Path) -> ValidationResult {
let start = Instant::now();
#[cfg(feature = "openqasm")]
if language == Language::OpenQASM {
return validate_qasm_native(concept_id, file_path);
}
let Some(cmd_template) = validation_command(language) else {
return ValidationResult {
concept_id: concept_id.to_string(),
language,
passed: false,
error: Some("no validation command for this language".into()),
duration_ms: 0,
};
};
let file_str = file_path.display().to_string();
let out_path = format!("/tmp/vidya_test_{}_{}", concept_id, std::process::id());
let cmd = cmd_template
.replace("{file}", &file_str)
.replace("{out}", &out_path);
let result = std::process::Command::new("sh")
.args(["-c", &cmd])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output();
let _ = std::fs::remove_file(&out_path);
let duration_ms = start.elapsed().as_millis() as u64;
match result {
Ok(output) => {
if output.status.success() {
ValidationResult {
concept_id: concept_id.to_string(),
language,
passed: true,
error: None,
duration_ms,
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let msg = if stderr.is_empty() {
stdout.into_owned()
} else {
stderr.into_owned()
};
ValidationResult {
concept_id: concept_id.to_string(),
language,
passed: false,
error: Some(msg),
duration_ms,
}
}
}
Err(e) => ValidationResult {
concept_id: concept_id.to_string(),
language,
passed: false,
error: Some(format!("failed to execute command: {e}")),
duration_ms,
},
}
}
pub fn validate_all(registry: &Registry, content_dir: &Path) -> Vec<ValidationResult> {
let mut results = Vec::new();
for concept in registry.list() {
for (lang, example) in &concept.examples {
let Some(ref source_path) = example.source_path else {
continue;
};
let file_path = content_dir.join(source_path);
if !file_path.exists() {
results.push(ValidationResult {
concept_id: concept.id.clone(),
language: *lang,
passed: false,
error: Some(format!("source file not found: {}", file_path.display())),
duration_ms: 0,
});
continue;
}
results.push(run_validation(&concept.id, *lang, &file_path));
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_commands_exist() {
for lang in Language::all() {
#[cfg(feature = "openqasm")]
if *lang == Language::OpenQASM {
assert!(
validation_command(*lang).is_none(),
"OpenQASM should use native validation, not a shell command"
);
continue;
}
assert!(
validation_command(*lang).is_some(),
"missing validation command for {lang}"
);
}
}
#[test]
fn validation_result_serde() {
let result = ValidationResult {
concept_id: "strings".into(),
language: Language::Rust,
passed: true,
error: None,
duration_ms: 42,
};
let json = serde_json::to_string(&result).unwrap();
let decoded: ValidationResult = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.concept_id, "strings");
assert!(decoded.passed);
}
#[test]
fn validation_result_failure() {
let result = ValidationResult {
concept_id: "concurrency".into(),
language: Language::C,
passed: false,
error: Some("segfault".into()),
duration_ms: 100,
};
assert!(!result.passed);
assert_eq!(result.error.as_deref(), Some("segfault"));
}
}