1use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15
16use crate::ast;
17use crate::ast::display::format_ast_type;
18use crate::cost::ProgramCost;
19use crate::hash::ContentHash;
20use crate::target::{Arch, TerrainConfig, UnionConfig};
21
22#[derive(Clone, Debug)]
26pub struct PackageManifest {
27 pub name: String,
28 pub version: String,
29 pub program_digest: String,
31 pub source_hash: String,
33 pub target_vm: String,
34 pub target_os: Option<String>,
35 pub architecture: String,
36 pub cost: ManifestCost,
37 pub functions: Vec<ManifestFunction>,
38 pub entry_point: String,
39 pub built_at: String,
41 pub compiler_version: String,
42}
43
44#[derive(Clone, Debug)]
45pub struct ManifestCost {
46 pub table_values: Vec<u64>,
48 pub table_names: Vec<String>,
50 pub padded_height: u64,
51}
52
53#[derive(Clone, Debug)]
54pub struct ManifestFunction {
55 pub name: String,
56 pub hash: String,
58 pub signature: String,
60}
61
62pub struct PackageResult {
64 pub manifest: PackageManifest,
65 pub artifact_dir: PathBuf,
66 pub tasm_path: PathBuf,
67 pub manifest_path: PathBuf,
68}
69
70pub fn generate_artifact(
77 name: &str,
78 version: &str,
79 tasm: &str,
80 source_file: &ast::File,
81 cost: &ProgramCost,
82 target_vm: &TerrainConfig,
83 target_os: Option<&UnionConfig>,
84 output_base: &Path,
85) -> Result<PackageResult, String> {
86 let digest_bytes = crate::poseidon2::hash_bytes(tasm.as_bytes());
88 let program_digest = ContentHash(digest_bytes);
89
90 let source_hash = crate::hash::hash_file_content(source_file);
92
93 let fn_hashes = crate::hash::hash_file(source_file);
95 let functions = extract_functions(source_file, &fn_hashes);
96
97 let entry_point = find_entry_point(source_file);
99
100 let architecture = match target_vm.architecture {
102 Arch::Stack => "stack",
103 Arch::Register => "register",
104 Arch::Tree => "tree",
105 }
106 .to_string();
107
108 let manifest = PackageManifest {
110 name: name.to_string(),
111 version: version.to_string(),
112 program_digest: program_digest.to_hex(),
113 source_hash: source_hash.to_hex(),
114 target_vm: target_vm.name.clone(),
115 target_os: target_os.map(|os| os.name.clone()),
116 architecture,
117 cost: ManifestCost {
118 table_values: (0..cost.total.count as usize)
119 .map(|i| cost.total.get(i))
120 .collect(),
121 table_names: cost.table_names.clone(),
122 padded_height: cost.padded_height,
123 },
124 functions,
125 entry_point,
126 built_at: iso8601_now(),
127 compiler_version: env!("CARGO_PKG_VERSION").to_string(),
128 };
129
130 let artifact_dir = output_base.join(format!("{}.deploy", name));
132 std::fs::create_dir_all(&artifact_dir)
133 .map_err(|e| format!("cannot create '{}': {}", artifact_dir.display(), e))?;
134
135 let tasm_path = artifact_dir.join("program.tasm");
137 std::fs::write(&tasm_path, tasm)
138 .map_err(|e| format!("cannot write '{}': {}", tasm_path.display(), e))?;
139
140 let manifest_path = artifact_dir.join("manifest.json");
142 std::fs::write(&manifest_path, manifest.to_json())
143 .map_err(|e| format!("cannot write '{}': {}", manifest_path.display(), e))?;
144
145 Ok(PackageResult {
146 manifest,
147 artifact_dir,
148 tasm_path,
149 manifest_path,
150 })
151}
152
153impl PackageManifest {
156 pub fn to_json(&self) -> String {
158 let mut out = String::from("{\n");
159
160 out.push_str(&format!(" \"name\": {},\n", json_string(&self.name)));
161 out.push_str(&format!(" \"version\": {},\n", json_string(&self.version)));
162 out.push_str(&format!(
163 " \"program_digest\": {},\n",
164 json_string(&self.program_digest)
165 ));
166 out.push_str(&format!(
167 " \"source_hash\": {},\n",
168 json_string(&self.source_hash)
169 ));
170
171 out.push_str(" \"target\": {\n");
173 out.push_str(&format!(" \"vm\": {},\n", json_string(&self.target_vm)));
174 if let Some(ref os) = self.target_os {
175 out.push_str(&format!(" \"os\": {},\n", json_string(os)));
176 } else {
177 out.push_str(" \"os\": null,\n");
178 }
179 out.push_str(&format!(
180 " \"architecture\": {}\n",
181 json_string(&self.architecture)
182 ));
183 out.push_str(" },\n");
184
185 out.push_str(" \"cost\": {\n");
187 for (i, name) in self.cost.table_names.iter().enumerate() {
188 let val = self.cost.table_values.get(i).copied().unwrap_or(0);
189 out.push_str(&format!(" {}: {},\n", json_string(name), val));
190 }
191 out.push_str(&format!(
192 " \"padded_height\": {}\n",
193 self.cost.padded_height
194 ));
195 out.push_str(" },\n");
196
197 out.push_str(" \"functions\": [\n");
199 for (i, func) in self.functions.iter().enumerate() {
200 let comma = if i + 1 < self.functions.len() {
201 ","
202 } else {
203 ""
204 };
205 out.push_str(&format!(
206 " {{ \"name\": {}, \"hash\": {}, \"signature\": {} }}{}\n",
207 json_string(&func.name),
208 json_string(&func.hash),
209 json_string(&func.signature),
210 comma,
211 ));
212 }
213 out.push_str(" ],\n");
214
215 out.push_str(&format!(
216 " \"entry_point\": {},\n",
217 json_string(&self.entry_point)
218 ));
219 out.push_str(&format!(
220 " \"built_at\": {},\n",
221 json_string(&self.built_at)
222 ));
223 out.push_str(&format!(
224 " \"compiler_version\": {}\n",
225 json_string(&self.compiler_version)
226 ));
227
228 out.push_str("}\n");
229 out
230 }
231}
232
233fn json_string(s: &str) -> String {
235 let mut out = String::from('"');
236 for ch in s.chars() {
237 match ch {
238 '"' => out.push_str("\\\""),
239 '\\' => out.push_str("\\\\"),
240 '\n' => out.push_str("\\n"),
241 '\r' => out.push_str("\\r"),
242 '\t' => out.push_str("\\t"),
243 c if (c as u32) < 0x20 => {
244 out.push_str(&format!("\\u{:04x}", c as u32));
245 }
246 c => out.push(c),
247 }
248 }
249 out.push('"');
250 out
251}
252
253fn extract_functions(
258 file: &ast::File,
259 fn_hashes: &BTreeMap<String, ContentHash>,
260) -> Vec<ManifestFunction> {
261 let mut functions = Vec::new();
262 for item in &file.items {
263 if let ast::Item::Fn(func) = &item.node {
264 if func.is_test {
265 continue;
266 }
267 let sig = format_fn_signature(func);
268 let hash = fn_hashes
269 .get(&func.name.node)
270 .map(|h| h.to_hex())
271 .unwrap_or_default();
272 functions.push(ManifestFunction {
273 name: func.name.node.clone(),
274 hash,
275 signature: sig,
276 });
277 }
278 }
279 functions
280}
281
282pub fn format_fn_signature(func: &ast::FnDef) -> String {
284 let mut sig = String::from("fn ");
285 sig.push_str(&func.name.node);
286
287 if !func.type_params.is_empty() {
288 let params: Vec<_> = func.type_params.iter().map(|p| p.node.clone()).collect();
289 sig.push_str(&format!("<{}>", params.join(", ")));
290 }
291
292 sig.push('(');
293 let params: Vec<String> = func
294 .params
295 .iter()
296 .map(|p| format!("{}: {}", p.name.node, format_ast_type(&p.ty.node)))
297 .collect();
298 sig.push_str(¶ms.join(", "));
299 sig.push(')');
300
301 if let Some(ref ret) = func.return_ty {
302 sig.push_str(&format!(" -> {}", format_ast_type(&ret.node)));
303 }
304
305 sig
306}
307
308fn find_entry_point(file: &ast::File) -> String {
310 for item in &file.items {
311 if let ast::Item::Fn(func) = &item.node {
312 if func.name.node == "main" {
313 return "main".to_string();
314 }
315 }
316 }
317 for item in &file.items {
319 if let ast::Item::Fn(func) = &item.node {
320 if !func.is_test {
321 return func.name.node.clone();
322 }
323 }
324 }
325 "main".to_string()
326}
327
328fn iso8601_now() -> String {
330 let secs = std::time::SystemTime::now()
331 .duration_since(std::time::UNIX_EPOCH)
332 .unwrap_or_default()
333 .as_secs();
334
335 let days = secs / 86400;
338 let time_of_day = secs % 86400;
339 let hours = time_of_day / 3600;
340 let minutes = (time_of_day % 3600) / 60;
341 let seconds = time_of_day % 60;
342
343 let (year, month, day) = days_to_date(days);
345
346 format!(
347 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
348 year, month, day, hours, minutes, seconds
349 )
350}
351
352pub fn days_to_date(days: u64) -> (u64, u64, u64) {
354 let z = days + 719468;
356 let era = z / 146097;
357 let doe = z - era * 146097;
358 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
359 let y = yoe + era * 400;
360 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
361 let mp = (5 * doy + 2) / 153;
362 let d = doy - (153 * mp + 2) / 5 + 1;
363 let m = if mp < 10 { mp + 3 } else { mp - 9 };
364 let y = if m <= 2 { y + 1 } else { y };
365 (y, m, d)
366}
367
368#[cfg(test)]
371mod tests;