use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::ast;
use crate::ast::display::format_ast_type;
use crate::cost::ProgramCost;
use crate::hash::ContentHash;
use crate::target::{Arch, TerrainConfig, UnionConfig};
#[derive(Clone, Debug)]
pub struct PackageManifest {
pub name: String,
pub version: String,
pub program_digest: String,
pub source_hash: String,
pub target_vm: String,
pub target_os: Option<String>,
pub architecture: String,
pub cost: ManifestCost,
pub functions: Vec<ManifestFunction>,
pub entry_point: String,
pub built_at: String,
pub compiler_version: String,
}
#[derive(Clone, Debug)]
pub struct ManifestCost {
pub table_values: Vec<u64>,
pub table_names: Vec<String>,
pub padded_height: u64,
}
#[derive(Clone, Debug)]
pub struct ManifestFunction {
pub name: String,
pub hash: String,
pub signature: String,
}
pub struct PackageResult {
pub manifest: PackageManifest,
pub artifact_dir: PathBuf,
pub tasm_path: PathBuf,
pub manifest_path: PathBuf,
}
pub fn generate_artifact(
name: &str,
version: &str,
tasm: &str,
source_file: &ast::File,
cost: &ProgramCost,
target_vm: &TerrainConfig,
target_os: Option<&UnionConfig>,
output_base: &Path,
) -> Result<PackageResult, String> {
let digest_bytes = crate::poseidon2::hash_bytes(tasm.as_bytes());
let program_digest = ContentHash(digest_bytes);
let source_hash = crate::hash::hash_file_content(source_file);
let fn_hashes = crate::hash::hash_file(source_file);
let functions = extract_functions(source_file, &fn_hashes);
let entry_point = find_entry_point(source_file);
let architecture = match target_vm.architecture {
Arch::Stack => "stack",
Arch::Register => "register",
Arch::Tree => "tree",
}
.to_string();
let manifest = PackageManifest {
name: name.to_string(),
version: version.to_string(),
program_digest: program_digest.to_hex(),
source_hash: source_hash.to_hex(),
target_vm: target_vm.name.clone(),
target_os: target_os.map(|os| os.name.clone()),
architecture,
cost: ManifestCost {
table_values: (0..cost.total.count as usize)
.map(|i| cost.total.get(i))
.collect(),
table_names: cost.table_names.clone(),
padded_height: cost.padded_height,
},
functions,
entry_point,
built_at: iso8601_now(),
compiler_version: env!("CARGO_PKG_VERSION").to_string(),
};
let artifact_dir = output_base.join(format!("{}.deploy", name));
std::fs::create_dir_all(&artifact_dir)
.map_err(|e| format!("cannot create '{}': {}", artifact_dir.display(), e))?;
let tasm_path = artifact_dir.join("program.tasm");
std::fs::write(&tasm_path, tasm)
.map_err(|e| format!("cannot write '{}': {}", tasm_path.display(), e))?;
let manifest_path = artifact_dir.join("manifest.json");
std::fs::write(&manifest_path, manifest.to_json())
.map_err(|e| format!("cannot write '{}': {}", manifest_path.display(), e))?;
Ok(PackageResult {
manifest,
artifact_dir,
tasm_path,
manifest_path,
})
}
impl PackageManifest {
pub fn to_json(&self) -> String {
let mut out = String::from("{\n");
out.push_str(&format!(" \"name\": {},\n", json_string(&self.name)));
out.push_str(&format!(" \"version\": {},\n", json_string(&self.version)));
out.push_str(&format!(
" \"program_digest\": {},\n",
json_string(&self.program_digest)
));
out.push_str(&format!(
" \"source_hash\": {},\n",
json_string(&self.source_hash)
));
out.push_str(" \"target\": {\n");
out.push_str(&format!(" \"vm\": {},\n", json_string(&self.target_vm)));
if let Some(ref os) = self.target_os {
out.push_str(&format!(" \"os\": {},\n", json_string(os)));
} else {
out.push_str(" \"os\": null,\n");
}
out.push_str(&format!(
" \"architecture\": {}\n",
json_string(&self.architecture)
));
out.push_str(" },\n");
out.push_str(" \"cost\": {\n");
for (i, name) in self.cost.table_names.iter().enumerate() {
let val = self.cost.table_values.get(i).copied().unwrap_or(0);
out.push_str(&format!(" {}: {},\n", json_string(name), val));
}
out.push_str(&format!(
" \"padded_height\": {}\n",
self.cost.padded_height
));
out.push_str(" },\n");
out.push_str(" \"functions\": [\n");
for (i, func) in self.functions.iter().enumerate() {
let comma = if i + 1 < self.functions.len() {
","
} else {
""
};
out.push_str(&format!(
" {{ \"name\": {}, \"hash\": {}, \"signature\": {} }}{}\n",
json_string(&func.name),
json_string(&func.hash),
json_string(&func.signature),
comma,
));
}
out.push_str(" ],\n");
out.push_str(&format!(
" \"entry_point\": {},\n",
json_string(&self.entry_point)
));
out.push_str(&format!(
" \"built_at\": {},\n",
json_string(&self.built_at)
));
out.push_str(&format!(
" \"compiler_version\": {}\n",
json_string(&self.compiler_version)
));
out.push_str("}\n");
out
}
}
fn json_string(s: &str) -> String {
let mut out = String::from('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
fn extract_functions(
file: &ast::File,
fn_hashes: &BTreeMap<String, ContentHash>,
) -> Vec<ManifestFunction> {
let mut functions = Vec::new();
for item in &file.items {
if let ast::Item::Fn(func) = &item.node {
if func.is_test {
continue;
}
let sig = format_fn_signature(func);
let hash = fn_hashes
.get(&func.name.node)
.map(|h| h.to_hex())
.unwrap_or_default();
functions.push(ManifestFunction {
name: func.name.node.clone(),
hash,
signature: sig,
});
}
}
functions
}
pub fn format_fn_signature(func: &ast::FnDef) -> String {
let mut sig = String::from("fn ");
sig.push_str(&func.name.node);
if !func.type_params.is_empty() {
let params: Vec<_> = func.type_params.iter().map(|p| p.node.clone()).collect();
sig.push_str(&format!("<{}>", params.join(", ")));
}
sig.push('(');
let params: Vec<String> = func
.params
.iter()
.map(|p| format!("{}: {}", p.name.node, format_ast_type(&p.ty.node)))
.collect();
sig.push_str(¶ms.join(", "));
sig.push(')');
if let Some(ref ret) = func.return_ty {
sig.push_str(&format!(" -> {}", format_ast_type(&ret.node)));
}
sig
}
fn find_entry_point(file: &ast::File) -> String {
for item in &file.items {
if let ast::Item::Fn(func) = &item.node {
if func.name.node == "main" {
return "main".to_string();
}
}
}
for item in &file.items {
if let ast::Item::Fn(func) = &item.node {
if !func.is_test {
return func.name.node.clone();
}
}
}
"main".to_string()
}
fn iso8601_now() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_date(days);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hours, minutes, seconds
)
}
pub fn days_to_date(days: u64) -> (u64, u64, u64) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests;