use anyhow::{Context, Result};
use std::fmt;
use std::process::Command;
use crate::manifest::Manifest;
#[derive(Debug, Clone)]
pub struct CompileCommand {
pub env: Vec<(String, String)>,
pub program: String,
pub args: Vec<String>,
}
impl CompileCommand {
pub fn display(&self) -> String {
let env_str: Vec<String> = self
.env
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
let parts: Vec<&str> = env_str
.iter()
.map(|s| s.as_str())
.chain(std::iter::once(self.program.as_str()))
.chain(self.args.iter().map(|s| s.as_str()))
.collect();
parts.join(" ")
}
}
impl fmt::Display for CompileCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display())
}
}
pub fn build_command(manifest: &Manifest, dats_file: &str, release: bool) -> Result<CompileCommand> {
let ats2 = &manifest.ats2;
let mut env = Vec::new();
if let Some(ref patshome) = ats2.patshome {
env.push(("PATSHOME".to_string(), patshome.clone()));
}
let mut args = Vec::new();
for flag in &ats2.flags {
args.push(flag.clone());
}
for cflag in &ats2.c_flags {
args.push("-ccopt".to_string());
args.push(cflag.clone());
}
if release {
args.push("-ccopt".to_string());
args.push("-O2".to_string());
}
let output_name = dats_file.trim_end_matches(".dats");
args.push("-o".to_string());
args.push(output_name.to_string());
args.push(dats_file.to_string());
Ok(CompileCommand {
env,
program: ats2.patscc.clone(),
args,
})
}
pub fn typecheck_command(manifest: &Manifest, dats_file: &str) -> Result<CompileCommand> {
let ats2 = &manifest.ats2;
let mut env = Vec::new();
if let Some(ref patshome) = ats2.patshome {
env.push(("PATSHOME".to_string(), patshome.clone()));
}
let mut args = vec![
"--typecheck".to_string(),
];
for flag in &ats2.flags {
args.push(flag.clone());
}
args.push(dats_file.to_string());
Ok(CompileCommand {
env,
program: ats2.patsopt.clone(),
args,
})
}
pub fn execute_build(cmd: &CompileCommand) -> Result<()> {
let mut command = Command::new(&cmd.program);
command.args(&cmd.args);
for (key, value) in &cmd.env {
command.env(key, value);
}
let output = command
.output()
.with_context(|| format!("Failed to execute: {}", cmd.display()))?;
if !output.stdout.is_empty() {
let stdout = String::from_utf8_lossy(&output.stdout);
print!("{}", stdout);
}
if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprint!("{}", stderr);
}
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
anyhow::bail!(
"ATS2 compilation failed with exit code {}.\n\
This typically means a linear type proof could not be discharged.\n\
Check the error messages above for details.",
code
);
}
Ok(())
}
pub fn execute_run(binary: &str, args: &[String]) -> Result<()> {
let output = Command::new(binary)
.args(args)
.output()
.with_context(|| format!("Failed to execute binary: {}", binary))?;
if !output.stdout.is_empty() {
let stdout = String::from_utf8_lossy(&output.stdout);
print!("{}", stdout);
}
if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprint!("{}", stderr);
}
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
anyhow::bail!("Binary exited with code {}", code);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{ATS2Config, CSource, Manifest, OwnershipRule, ProjectConfig};
fn test_manifest() -> Manifest {
Manifest {
project: ProjectConfig {
name: "test-lib".to_string(),
version: "0.1.0".to_string(),
description: "Test".to_string(),
output_dir: "generated/ats".to_string(),
},
c_sources: vec![CSource {
path: "test.h".to_string(),
include_dirs: vec!["include".to_string()],
description: String::new(),
}],
ownership_rules: vec![OwnershipRule {
function: "test_alloc".to_string(),
pattern: "alloc".to_string(),
param_index: None,
resource_type: Some("test_t".to_string()),
description: String::new(),
}],
ats2: ATS2Config {
patsopt: "patsopt".to_string(),
patscc: "patscc".to_string(),
flags: vec!["-DATS_MEMALLOC_LIBC".to_string()],
c_flags: vec!["-O2".to_string()],
patshome: Some("/usr/lib/ats2".to_string()),
},
workload: None,
data: None,
options: None,
}
}
#[test]
fn test_build_command_structure() {
let manifest = test_manifest();
let cmd = build_command(&manifest, "generated/ats/test_safe.dats", false).unwrap();
assert_eq!(cmd.program, "patscc");
assert!(cmd.args.contains(&"-DATS_MEMALLOC_LIBC".to_string()));
assert!(cmd.args.contains(&"generated/ats/test_safe.dats".to_string()));
assert!(cmd.env.iter().any(|(k, _)| k == "PATSHOME"));
}
#[test]
fn test_build_command_release() {
let manifest = test_manifest();
let cmd = build_command(&manifest, "test.dats", true).unwrap();
let ccopt_indices: Vec<usize> = cmd
.args
.iter()
.enumerate()
.filter(|(_, a)| *a == "-ccopt")
.map(|(i, _)| i)
.collect();
let has_o2 = ccopt_indices
.iter()
.any(|&i| i + 1 < cmd.args.len() && cmd.args[i + 1] == "-O2");
assert!(has_o2, "Release mode should add -ccopt -O2");
}
#[test]
fn test_typecheck_command() {
let manifest = test_manifest();
let cmd = typecheck_command(&manifest, "test.dats").unwrap();
assert_eq!(cmd.program, "patsopt");
assert!(cmd.args.contains(&"--typecheck".to_string()));
assert!(cmd.args.contains(&"test.dats".to_string()));
}
#[test]
fn test_command_display() {
let cmd = CompileCommand {
env: vec![("PATSHOME".to_string(), "/usr/lib/ats2".to_string())],
program: "patscc".to_string(),
args: vec!["-o".to_string(), "out".to_string(), "in.dats".to_string()],
};
let display = cmd.display();
assert!(display.contains("PATSHOME=/usr/lib/ats2"));
assert!(display.contains("patscc"));
assert!(display.contains("-o out in.dats"));
}
#[test]
fn test_build_command_without_patshome() {
let mut manifest = test_manifest();
manifest.ats2.patshome = None;
let cmd = build_command(&manifest, "test.dats", false).unwrap();
assert!(cmd.env.is_empty());
}
}