use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub project: ProjectConfig,
#[serde(rename = "c-sources", default)]
pub c_sources: Vec<CSource>,
#[serde(rename = "ownership-rules", default)]
pub ownership_rules: Vec<OwnershipRule>,
#[serde(default)]
pub ats2: ATS2Config,
#[serde(default)]
pub workload: Option<WorkloadConfig>,
#[serde(default)]
pub data: Option<DataConfig>,
#[serde(default)]
pub options: Option<LegacyOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub description: String,
#[serde(rename = "output-dir", default = "default_output_dir")]
pub output_dir: String,
}
fn default_version() -> String {
"0.1.0".to_string()
}
fn default_output_dir() -> String {
"generated/ats".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CSource {
pub path: String,
#[serde(rename = "include-dirs", default)]
pub include_dirs: Vec<String>,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnershipRule {
pub function: String,
pub pattern: String,
#[serde(default)]
pub param_index: Option<usize>,
#[serde(rename = "resource-type", default)]
pub resource_type: Option<String>,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ATS2Config {
#[serde(default = "default_patsopt")]
pub patsopt: String,
#[serde(default = "default_patscc")]
pub patscc: String,
#[serde(default)]
pub flags: Vec<String>,
#[serde(rename = "c-flags", default)]
pub c_flags: Vec<String>,
#[serde(default)]
pub patshome: Option<String>,
}
fn default_patsopt() -> String {
"patsopt".to_string()
}
fn default_patscc() -> String {
"patscc".to_string()
}
impl Default for ATS2Config {
fn default() -> Self {
Self {
patsopt: default_patsopt(),
patscc: default_patscc(),
flags: Vec::new(),
c_flags: Vec::new(),
patshome: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkloadConfig {
pub name: String,
pub entry: String,
#[serde(default)]
pub strategy: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataConfig {
#[serde(rename = "input-type")]
pub input_type: String,
#[serde(rename = "output-type")]
pub output_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LegacyOptions {
#[serde(default)]
pub flags: Vec<String>,
}
pub fn load_manifest(path: &str) -> Result<Manifest> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read manifest: {}", path))?;
toml::from_str(&content)
.with_context(|| format!("Failed to parse manifest: {}", path))
}
pub fn validate(manifest: &Manifest) -> Result<()> {
if manifest.project.name.is_empty() {
anyhow::bail!("project.name is required");
}
for (i, source) in manifest.c_sources.iter().enumerate() {
if source.path.is_empty() {
anyhow::bail!("c-sources[{}].path is required", i);
}
}
let valid_patterns = ["alloc", "free", "borrow", "transfer"];
for (i, rule) in manifest.ownership_rules.iter().enumerate() {
if rule.function.is_empty() {
anyhow::bail!("ownership-rules[{}].function is required", i);
}
if !valid_patterns.contains(&rule.pattern.as_str()) {
anyhow::bail!(
"ownership-rules[{}].pattern must be one of: alloc, free, borrow, transfer (got: '{}')",
i,
rule.pattern
);
}
}
if manifest.ats2.patsopt.is_empty() {
anyhow::bail!("ats2.patsopt must not be empty");
}
Ok(())
}
pub fn init_manifest(path: &str) -> Result<()> {
let manifest_path = Path::new(path).join("atsiser.toml");
if manifest_path.exists() {
anyhow::bail!("atsiser.toml already exists");
}
let template = r#"# atsiser manifest — C to ATS2 linear type wrapping
# SPDX-License-Identifier: PMPL-1.0-or-later
[project]
name = "my-c-library"
version = "0.1.0"
description = "ATS2 linear type wrappers for my-c-library"
output-dir = "generated/ats"
# C source files to analyse
[[c-sources]]
path = "include/mylib.h"
include-dirs = ["include"]
description = "Main library header"
# Ownership rules — annotate how each C function handles memory
[[ownership-rules]]
function = "mylib_create"
pattern = "alloc"
resource-type = "mylib_t"
description = "Allocates a new mylib handle"
[[ownership-rules]]
function = "mylib_destroy"
pattern = "free"
param-index = 0
resource-type = "mylib_t"
description = "Frees a mylib handle"
[[ownership-rules]]
function = "mylib_process"
pattern = "borrow"
param-index = 0
description = "Borrows the handle for processing"
# ATS2 compiler configuration
[ats2]
patsopt = "patsopt"
patscc = "patscc"
flags = ["-DATS_MEMALLOC_LIBC"]
c-flags = ["-O2"]
"#;
std::fs::write(&manifest_path, template)?;
println!("Created {}", manifest_path.display());
Ok(())
}
pub fn print_info(manifest: &Manifest) {
println!("=== {} v{} ===", manifest.project.name, manifest.project.version);
println!("Description: {}", manifest.project.description);
println!("Output dir: {}", manifest.project.output_dir);
println!();
println!("C Sources ({}):", manifest.c_sources.len());
for src in &manifest.c_sources {
println!(" - {} (includes: {:?})", src.path, src.include_dirs);
}
println!();
println!("Ownership Rules ({}):", manifest.ownership_rules.len());
for rule in &manifest.ownership_rules {
println!(" - {} [{}]{}", rule.function, rule.pattern,
rule.resource_type.as_deref().map(|t| format!(" -> {}", t)).unwrap_or_default()
);
}
println!();
println!("ATS2 Config:");
println!(" patsopt: {}", manifest.ats2.patsopt);
println!(" patscc: {}", manifest.ats2.patscc);
if !manifest.ats2.flags.is_empty() {
println!(" flags: {:?}", manifest.ats2.flags);
}
if !manifest.ats2.c_flags.is_empty() {
println!(" c-flags: {:?}", manifest.ats2.c_flags);
}
}
pub fn project_name(manifest: &Manifest) -> &str {
&manifest.project.name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_manifest() {
let toml_str = r#"
[project]
name = "test"
version = "1.0.0"
description = "A test project"
[[c-sources]]
path = "test.h"
include-dirs = ["/usr/include"]
[[ownership-rules]]
function = "malloc"
pattern = "alloc"
[ats2]
patsopt = "patsopt"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.project.name, "test");
assert_eq!(manifest.c_sources.len(), 1);
assert_eq!(manifest.ownership_rules.len(), 1);
}
#[test]
fn test_validate_empty_name() {
let manifest = Manifest {
project: ProjectConfig {
name: String::new(),
version: "0.1.0".to_string(),
description: String::new(),
output_dir: "out".to_string(),
},
c_sources: vec![],
ownership_rules: vec![],
ats2: ATS2Config::default(),
workload: None,
data: None,
options: None,
};
assert!(validate(&manifest).is_err());
}
#[test]
fn test_validate_invalid_pattern() {
let manifest = Manifest {
project: ProjectConfig {
name: "test".to_string(),
version: "0.1.0".to_string(),
description: String::new(),
output_dir: "out".to_string(),
},
c_sources: vec![],
ownership_rules: vec![OwnershipRule {
function: "foo".to_string(),
pattern: "invalid".to_string(),
param_index: None,
resource_type: None,
description: String::new(),
}],
ats2: ATS2Config::default(),
workload: None,
data: None,
options: None,
};
let err = validate(&manifest).unwrap_err();
assert!(err.to_string().contains("invalid"));
}
#[test]
fn test_validate_valid_manifest() {
let manifest = Manifest {
project: ProjectConfig {
name: "test".to_string(),
version: "0.1.0".to_string(),
description: "A test".to_string(),
output_dir: "out".to_string(),
},
c_sources: vec![CSource {
path: "test.h".to_string(),
include_dirs: vec![],
description: String::new(),
}],
ownership_rules: vec![OwnershipRule {
function: "malloc".to_string(),
pattern: "alloc".to_string(),
param_index: None,
resource_type: Some("void".to_string()),
description: String::new(),
}],
ats2: ATS2Config::default(),
workload: None,
data: None,
options: None,
};
assert!(validate(&manifest).is_ok());
}
}