Skip to main content

trident/deploy/
mod.rs

1//! Packaging: produce a self-contained artifact for Trident programs.
2//!
3//! `trident package` creates a `.deploy/` directory containing the compiled
4//! TASM and a `manifest.json` with metadata:
5//! - `program_digest` — Poseidon2 hash of compiled TASM (what verifiers check)
6//! - `source_hash` — content hash of the source AST
7//! - target info (VM + optional OS)
8//! - cost analysis
9//! - function signatures with per-function content hashes
10//!
11//! The packaged artifact can then be deployed via `trident deploy`.
12
13use 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// ─── Data Types ────────────────────────────────────────────────────
23
24/// Package manifest — all metadata about a packaged program artifact.
25#[derive(Clone, Debug)]
26pub struct PackageManifest {
27    pub name: String,
28    pub version: String,
29    /// Poseidon2 hash of the compiled TASM bytes (hex).
30    pub program_digest: String,
31    /// Content hash of the source AST (hex).
32    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    /// ISO 8601 timestamp.
40    pub built_at: String,
41    pub compiler_version: String,
42}
43
44#[derive(Clone, Debug)]
45pub struct ManifestCost {
46    /// Cost values per table, indexed by position.
47    pub table_values: Vec<u64>,
48    /// Table names for serialization (e.g. ["processor", "hash", "u32", ...]).
49    pub table_names: Vec<String>,
50    pub padded_height: u64,
51}
52
53#[derive(Clone, Debug)]
54pub struct ManifestFunction {
55    pub name: String,
56    /// Content hash (hex).
57    pub hash: String,
58    /// Signature string (e.g. "fn pay(from: Digest, amount: Field)").
59    pub signature: String,
60}
61
62/// Result of a package operation.
63pub struct PackageResult {
64    pub manifest: PackageManifest,
65    pub artifact_dir: PathBuf,
66    pub tasm_path: PathBuf,
67    pub manifest_path: PathBuf,
68}
69
70// ─── Artifact Generation ───────────────────────────────────────────
71
72/// Generate a package artifact from a compiled project.
73///
74/// Creates a `<name>.deploy/` directory under `output_base` containing
75/// `program.tasm` and `manifest.json`.
76pub 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    // 1. Compute program_digest = Poseidon2(tasm bytes)
87    let digest_bytes = crate::poseidon2::hash_bytes(tasm.as_bytes());
88    let program_digest = ContentHash(digest_bytes);
89
90    // 2. Compute source_hash from AST
91    let source_hash = crate::hash::hash_file_content(source_file);
92
93    // 3. Extract function signatures + per-function hashes
94    let fn_hashes = crate::hash::hash_file(source_file);
95    let functions = extract_functions(source_file, &fn_hashes);
96
97    // 4. Determine entry point
98    let entry_point = find_entry_point(source_file);
99
100    // 5. Architecture string
101    let architecture = match target_vm.architecture {
102        Arch::Stack => "stack",
103        Arch::Register => "register",
104        Arch::Tree => "tree",
105    }
106    .to_string();
107
108    // 6. Build manifest
109    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    // 7. Create artifact directory
131    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    // 8. Write program.tasm
136    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    // 9. Write manifest.json
141    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
153// ─── JSON Serialization ────────────────────────────────────────────
154
155impl PackageManifest {
156    /// Serialize to JSON (hand-rolled, no serde dependency).
157    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        // target object
172        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        // cost object
186        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        // functions array
198        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
233/// JSON-escape a string and wrap in quotes.
234fn 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
253// ─── Helpers ───────────────────────────────────────────────────────
254
255/// Extract function names, signatures, and hashes from a parsed file.
256/// Skips test functions.
257fn 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
282/// Format a function signature for the manifest.
283pub 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(&params.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
308/// Find the entry point function name (looks for "main").
309fn 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    // Fallback: first non-test function
318    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
328/// Get current time as ISO 8601 string (no chrono dependency).
329fn 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    // Convert epoch seconds to a basic ISO 8601 date-time.
336    // This is a simplified conversion (no leap second handling).
337    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    // Calculate year/month/day from days since epoch (1970-01-01).
344    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
352/// Convert days since Unix epoch to (year, month, day).
353pub fn days_to_date(days: u64) -> (u64, u64, u64) {
354    // Algorithm from https://howardhinnant.github.io/date_algorithms.html
355    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// ─── Tests ─────────────────────────────────────────────────────────
369
370#[cfg(test)]
371mod tests;