Skip to main content

trident/config/target/
mod.rs

1use std::path::Path;
2
3use crate::diagnostic::Diagnostic;
4use crate::span::Span;
5
6/// VM architecture family.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Arch {
9    /// Stack machine (Triton VM, Miden VM): direct emission, no IR.
10    Stack,
11    /// Register machine (Cairo, RISC-V zkVMs): requires lightweight IR.
12    Register,
13    /// Tree machine (Nock): combinator-based, subject-formula evaluation.
14    Tree,
15}
16
17/// Describes a non-native field the target can emulate.
18#[derive(Clone, Debug)]
19pub struct EmulatedField {
20    /// Short identifier (e.g. "bn254", "stark252").
21    pub name: String,
22    /// Field size in bits.
23    pub bits: u32,
24    /// Number of native field elements per emulated element.
25    pub limbs: u32,
26}
27
28/// Optional warrior configuration for a target VM.
29///
30/// Warriors are external binaries that handle execution, proving, and
31/// deployment for a specific VM. The `[warrior]` section in target.toml
32/// tells Trident which warrior to look for on PATH.
33#[derive(Clone, Debug)]
34pub struct WarriorConfig {
35    /// Warrior name (e.g. "trisha").
36    pub name: String,
37    /// Cargo crate name (e.g. "trident-trisha").
38    pub crate_name: String,
39    /// Whether this warrior supports `run` (execution).
40    pub runner: bool,
41    /// Whether this warrior supports `prove` (proof generation).
42    pub prover: bool,
43}
44
45/// Target VM configuration — replaces all hardcoded constants.
46///
47/// Every numeric constant that was previously hardcoded for Triton VM
48/// (stack depth 16, digest width 5, hash rate 10, etc.) now lives here.
49#[derive(Clone, Debug)]
50pub struct TerrainConfig {
51    /// Short identifier used in CLI and file paths (e.g. "triton").
52    pub name: String,
53    /// Human-readable name (e.g. "Triton VM").
54    pub display_name: String,
55    /// Architecture family.
56    pub architecture: Arch,
57    /// Field prime description (informational, e.g. "2^64 - 2^32 + 1").
58    pub field_prime: String,
59    /// Native field size in bits (e.g. 64 for Goldilocks, 31 for Mersenne31).
60    pub field_bits: u32,
61    /// Number of U32 limbs when splitting a field element.
62    pub field_limbs: u32,
63    /// Non-native fields this target can emulate (empty = native only).
64    pub emulated_fields: Vec<EmulatedField>,
65    /// Maximum operand stack depth before spilling to RAM.
66    pub stack_depth: u32,
67    /// Base RAM address for spilled variables.
68    pub spill_ram_base: u64,
69    /// Width of a hash digest in field elements.
70    pub digest_width: u32,
71    /// Degree of the extension field (0 if no extension field support).
72    pub xfield_width: u32,
73    /// Hash function absorption rate in field elements.
74    pub hash_rate: u32,
75    /// File extension for compiled output (e.g. ".tasm").
76    pub output_extension: String,
77    /// Names of the cost model tables (e.g. ["processor", "hash", ...]).
78    pub cost_tables: Vec<String>,
79    /// Optional warrior configuration for runtime/proving delegation.
80    pub warrior: Option<WarriorConfig>,
81}
82
83impl TerrainConfig {
84    /// Built-in Triton VM configuration (hardcoded fallback).
85    pub fn triton() -> Self {
86        Self {
87            name: "triton".to_string(),
88            display_name: "Triton VM".to_string(),
89            architecture: Arch::Stack,
90            field_prime: "2^64 - 2^32 + 1".to_string(),
91            field_bits: 64,
92            field_limbs: 2,
93            emulated_fields: Vec::new(),
94            stack_depth: 16,
95            spill_ram_base: 1 << 30,
96            digest_width: 5,
97            xfield_width: 3,
98            hash_rate: 10,
99            output_extension: ".tasm".to_string(),
100            cost_tables: vec![
101                "processor".to_string(),
102                "hash".to_string(),
103                "u32".to_string(),
104                "op_stack".to_string(),
105                "ram".to_string(),
106                "jump_stack".to_string(),
107            ],
108            warrior: Some(WarriorConfig {
109                name: "trisha".to_string(),
110                crate_name: "trident-trisha".to_string(),
111                runner: true,
112                prover: true,
113            }),
114        }
115    }
116
117    /// Load a target configuration from a TOML file.
118    pub fn load(path: &Path) -> Result<Self, Diagnostic> {
119        let content = std::fs::read_to_string(path).map_err(|e| {
120            Diagnostic::error(
121                format!("cannot read target config '{}': {}", path.display(), e),
122                Span::dummy(),
123            )
124        })?;
125        Self::parse_toml(&content, path)
126    }
127
128    /// Resolve a target by name: look for `vm/{name}.toml` relative to
129    /// the compiler binary or working directory, falling back to built-in configs.
130    pub fn resolve(name: &str) -> Result<Self, Diagnostic> {
131        // Reject path traversal
132        if name.contains('/') || name.contains('\\') || name.contains("..") || name.starts_with('.')
133        {
134            return Err(Diagnostic::error(
135                format!("invalid target name '{}'", name),
136                Span::dummy(),
137            ));
138        }
139
140        // Built-in target
141        if name == "triton" {
142            return Ok(Self::triton());
143        }
144
145        // Search for vm/{name}/target.toml first, then vm/{name}.toml (legacy)
146        let primary = format!("vm/{}/target.toml", name);
147        let fallback = format!("vm/{}.toml", name);
148
149        // 1. Relative to compiler binary
150        if let Ok(exe) = std::env::current_exe() {
151            if let Some(dir) = exe.parent() {
152                for ancestor in &[
153                    Some(dir.to_path_buf()),
154                    dir.parent().map(|p| p.to_path_buf()),
155                    dir.parent()
156                        .and_then(|p| p.parent())
157                        .map(|p| p.to_path_buf()),
158                ] {
159                    if let Some(base) = ancestor {
160                        let path = base.join(&primary);
161                        if path.exists() {
162                            return Self::load(&path);
163                        }
164                        let path = base.join(&fallback);
165                        if path.exists() {
166                            return Self::load(&path);
167                        }
168                    }
169                }
170            }
171        }
172
173        // 2. Current working directory
174        let cwd_path = std::path::PathBuf::from(&primary);
175        if cwd_path.exists() {
176            return Self::load(&cwd_path);
177        }
178        let cwd_path = std::path::PathBuf::from(&fallback);
179        if cwd_path.exists() {
180            return Self::load(&cwd_path);
181        }
182
183        Err(Diagnostic::error(
184            format!("unknown target '{}' (looked for '{}')", name, primary),
185            Span::dummy(),
186        )
187        .with_help("available targets: triton, miden, openvm, sp1, cairo, nock".to_string()))
188    }
189
190    fn parse_toml(content: &str, path: &Path) -> Result<Self, Diagnostic> {
191        let err =
192            |msg: String| Diagnostic::error(format!("{}: {}", path.display(), msg), Span::dummy());
193
194        let mut name = String::new();
195        let mut display_name = String::new();
196        let mut architecture = String::new();
197        let mut output_extension = String::new();
198        let mut field_prime = String::new();
199        let mut field_bits: u32 = 0;
200        let mut field_limbs: u32 = 0;
201        let mut emulated_fields: Vec<EmulatedField> = Vec::new();
202        let mut stack_depth: u32 = 0;
203        let mut spill_ram_base: u64 = 0;
204        let mut digest_width: u32 = 0;
205        let mut hash_rate: u32 = 0;
206        let mut xfield_degree: u32 = 0;
207        let mut cost_tables: Vec<String> = Vec::new();
208        let mut warrior_name = String::new();
209        let mut warrior_crate = String::new();
210        let mut warrior_runner = false;
211        let mut warrior_prover = false;
212
213        let mut section = String::new();
214
215        for line in content.lines() {
216            let trimmed = line.trim();
217            if trimmed.is_empty() || trimmed.starts_with('#') {
218                continue;
219            }
220            if trimmed.starts_with('[') && trimmed.ends_with(']') {
221                section = trimmed[1..trimmed.len() - 1].trim().to_string();
222                continue;
223            }
224            if let Some((key, value)) = trimmed.split_once('=') {
225                let key = key.trim();
226                let value = value.trim();
227                let unquoted = value.trim_matches('"');
228
229                match (section.as_str(), key) {
230                    ("target", "name") => name = unquoted.to_string(),
231                    ("target", "display_name") => display_name = unquoted.to_string(),
232                    ("target", "architecture") => architecture = unquoted.to_string(),
233                    ("target", "output_extension") => output_extension = unquoted.to_string(),
234                    ("field", "prime") => field_prime = unquoted.to_string(),
235                    ("field", "bits") => {
236                        field_bits = value
237                            .parse()
238                            .map_err(|_| err(format!("invalid field.bits: {}", value)))?;
239                    }
240                    ("field", "limbs") => {
241                        field_limbs = value
242                            .parse()
243                            .map_err(|_| err(format!("invalid field.limbs: {}", value)))?;
244                    }
245                    ("stack", "depth") => {
246                        stack_depth = value
247                            .parse()
248                            .map_err(|_| err(format!("invalid stack.depth: {}", value)))?;
249                    }
250                    ("stack", "spill_ram_base") => {
251                        spill_ram_base = value
252                            .parse()
253                            .map_err(|_| err(format!("invalid stack.spill_ram_base: {}", value)))?;
254                    }
255                    ("hash", "digest_width") => {
256                        digest_width = value
257                            .parse()
258                            .map_err(|_| err(format!("invalid hash.digest_width: {}", value)))?;
259                    }
260                    ("hash", "rate") => {
261                        hash_rate = value
262                            .parse()
263                            .map_err(|_| err(format!("invalid hash.rate: {}", value)))?;
264                    }
265                    ("extension_field", "degree") => {
266                        xfield_degree = value.parse().map_err(|_| {
267                            err(format!("invalid extension_field.degree: {}", value))
268                        })?;
269                    }
270                    ("cost", "tables") => {
271                        cost_tables = parse_string_array(value);
272                    }
273                    ("warrior", "name") => warrior_name = unquoted.to_string(),
274                    ("warrior", "crate") => warrior_crate = unquoted.to_string(),
275                    ("warrior", "runner") => warrior_runner = value == "true",
276                    ("warrior", "prover") => warrior_prover = value == "true",
277                    _ => {
278                        // Parse [emulated_field.NAME] sections
279                        if section.starts_with("emulated_field.") {
280                            let ef_name = section
281                                .strip_prefix("emulated_field.")
282                                .expect("guarded by starts_with check");
283                            // Find or create the entry
284                            let entry = emulated_fields.iter_mut().find(|ef| ef.name == ef_name);
285                            let entry = if let Some(e) = entry {
286                                e
287                            } else {
288                                emulated_fields.push(EmulatedField {
289                                    name: ef_name.to_string(),
290                                    bits: 0,
291                                    limbs: 0,
292                                });
293                                emulated_fields.last_mut().expect("just pushed")
294                            };
295                            match key {
296                                "bits" => {
297                                    entry.bits = value.parse().map_err(|_| {
298                                        err(format!(
299                                            "invalid emulated_field.{}.bits: {}",
300                                            ef_name, value
301                                        ))
302                                    })?;
303                                }
304                                "limbs" => {
305                                    entry.limbs = value.parse().map_err(|_| {
306                                        err(format!(
307                                            "invalid emulated_field.{}.limbs: {}",
308                                            ef_name, value
309                                        ))
310                                    })?;
311                                }
312                                _ => {}
313                            }
314                        }
315                    }
316                }
317            }
318        }
319
320        if name.is_empty() {
321            return Err(err("missing target.name".to_string()));
322        }
323        if stack_depth == 0 {
324            return Err(err("stack.depth must be > 0".to_string()));
325        }
326        if digest_width == 0 {
327            return Err(err("hash.digest_width must be > 0".to_string()));
328        }
329        if hash_rate == 0 {
330            return Err(err("hash.rate must be > 0".to_string()));
331        }
332        if field_bits == 0 {
333            return Err(err("field.bits must be > 0".to_string()));
334        }
335        if field_limbs == 0 {
336            return Err(err("field.limbs must be > 0".to_string()));
337        }
338
339        let arch = match architecture.as_str() {
340            "stack" => Arch::Stack,
341            "register" => Arch::Register,
342            "tree" => Arch::Tree,
343            other => {
344                return Err(err(format!(
345                    "unknown architecture '{}' (expected 'stack', 'register', or 'tree')",
346                    other
347                )))
348            }
349        };
350
351        Ok(Self {
352            name,
353            display_name,
354            architecture: arch,
355            field_prime,
356            field_bits,
357            field_limbs,
358            emulated_fields,
359            stack_depth,
360            spill_ram_base,
361            digest_width,
362            xfield_width: xfield_degree,
363            hash_rate,
364            output_extension,
365            cost_tables,
366            warrior: if warrior_name.is_empty() {
367                None
368            } else {
369                let default_crate = format!("trident-{}", warrior_name);
370                Some(WarriorConfig {
371                    name: warrior_name,
372                    crate_name: if warrior_crate.is_empty() {
373                        default_crate
374                    } else {
375                        warrior_crate
376                    },
377                    runner: warrior_runner,
378                    prover: warrior_prover,
379                })
380            },
381        })
382    }
383}
384
385mod state;
386pub use state::*;
387
388mod os;
389pub use os::*;
390
391#[cfg(test)]
392mod tests;