Skip to main content

trident/config/target/
os.rs

1use super::*;
2
3// ─── OS Target Configuration ───────────────────────────────────────
4
5/// OS target configuration parsed from `os/<name>/target.toml`.
6///
7/// An OS target describes a blockchain or runtime environment that
8/// runs on top of a VM. The `vm` field maps the OS to its underlying
9/// VM (e.g. "neptune" → "triton", "starknet" → "cairo").
10#[derive(Clone, Debug)]
11pub struct UnionConfig {
12    /// OS name (e.g. "neptune").
13    pub name: String,
14    /// Display name (e.g. "Neptune").
15    pub display_name: String,
16    /// Underlying VM name (e.g. "triton").
17    pub vm: String,
18    /// Runtime binding prefix (e.g. "os.neptune").
19    pub binding_prefix: String,
20    /// Account model (e.g. "utxo", "account").
21    pub account_model: String,
22    /// Storage model (e.g. "merkle-authenticated", "key-value").
23    pub storage_model: String,
24    /// Transaction model (e.g. "proof-based", "signed").
25    pub transaction_model: String,
26}
27
28impl UnionConfig {
29    /// Try to resolve an OS config by name.
30    ///
31    /// Searches for `os/<name>/target.toml` relative to the compiler
32    /// binary and the current working directory.
33    /// Returns `Ok(None)` if no OS config file exists for this name.
34    /// Returns `Err` if the file exists but is malformed.
35    pub fn resolve(name: &str) -> Result<Option<Self>, Diagnostic> {
36        // Reject path traversal
37        if name.contains('/') || name.contains('\\') || name.contains("..") || name.starts_with('.')
38        {
39            return Ok(None);
40        }
41
42        let target_path = format!("os/{}/target.toml", name);
43
44        // 1. Relative to compiler binary
45        if let Ok(exe) = std::env::current_exe() {
46            if let Some(dir) = exe.parent() {
47                for ancestor in &[
48                    Some(dir.to_path_buf()),
49                    dir.parent().map(|p| p.to_path_buf()),
50                    dir.parent()
51                        .and_then(|p| p.parent())
52                        .map(|p| p.to_path_buf()),
53                ] {
54                    if let Some(base) = ancestor {
55                        let path = base.join(&target_path);
56                        if path.exists() {
57                            return Self::load(&path).map(Some);
58                        }
59                    }
60                }
61            }
62        }
63
64        // 2. Current working directory
65        let cwd_path = std::path::PathBuf::from(&target_path);
66        if cwd_path.exists() {
67            return Self::load(&cwd_path).map(Some);
68        }
69
70        Ok(None)
71    }
72
73    /// Load an OS config from a TOML file.
74    pub fn load(path: &Path) -> Result<Self, Diagnostic> {
75        let content = std::fs::read_to_string(path).map_err(|e| {
76            Diagnostic::error(
77                format!("cannot read OS config '{}': {}", path.display(), e),
78                Span::dummy(),
79            )
80        })?;
81        Self::parse_toml(&content, path)
82    }
83
84    fn parse_toml(content: &str, path: &Path) -> Result<Self, Diagnostic> {
85        let err =
86            |msg: String| Diagnostic::error(format!("{}: {}", path.display(), msg), Span::dummy());
87
88        let mut name = String::new();
89        let mut display_name = String::new();
90        let mut vm = String::new();
91        let mut binding_prefix = String::new();
92        let mut account_model = String::new();
93        let mut storage_model = String::new();
94        let mut transaction_model = String::new();
95
96        let mut section = String::new();
97
98        for line in content.lines() {
99            let trimmed = line.trim();
100            if trimmed.is_empty() || trimmed.starts_with('#') {
101                continue;
102            }
103            if trimmed.starts_with('[') && trimmed.ends_with(']') {
104                section = trimmed[1..trimmed.len() - 1].trim().to_string();
105                continue;
106            }
107            if let Some((key, value)) = trimmed.split_once('=') {
108                let key = key.trim();
109                let unquoted = value.trim().trim_matches('"');
110
111                match (section.as_str(), key) {
112                    ("os", "name") => name = unquoted.to_string(),
113                    ("os", "display_name") => display_name = unquoted.to_string(),
114                    ("os", "vm") => vm = unquoted.to_string(),
115                    ("runtime", "binding_prefix") => binding_prefix = unquoted.to_string(),
116                    ("runtime", "account_model") => account_model = unquoted.to_string(),
117                    ("runtime", "storage_model") => storage_model = unquoted.to_string(),
118                    ("runtime", "transaction_model") => transaction_model = unquoted.to_string(),
119                    _ => {}
120                }
121            }
122        }
123
124        if name.is_empty() {
125            return Err(err("missing os.name".to_string()));
126        }
127        if vm.is_empty() {
128            return Err(err("missing os.vm".to_string()));
129        }
130
131        Ok(Self {
132            name,
133            display_name,
134            vm,
135            binding_prefix,
136            account_model,
137            storage_model,
138            transaction_model,
139        })
140    }
141}
142
143// ─── Combined Target Resolution ────────────────────────────────────
144
145/// Resolved target: either a bare VM or an OS+VM combination.
146///
147/// When the user passes `--target neptune`, we load the OS config first
148/// (which tells us the VM is "triton"), then load the VM config. When
149/// they pass `--target triton`, we load the VM config directly.
150#[derive(Clone, Debug)]
151pub struct ResolvedTarget {
152    /// VM configuration (always present).
153    pub vm: TerrainConfig,
154    /// OS configuration (present only if the target name was an OS).
155    pub os: Option<UnionConfig>,
156    /// State configuration (present only if explicitly specified).
157    pub state: Option<StateConfig>,
158}
159
160impl ResolvedTarget {
161    /// Resolve a target name: try OS first, then fall back to VM.
162    ///
163    /// This matches the resolution order from `reference/targets.md`:
164    /// 1. Is `<name>` an OS? Load `os/<name>/target.toml`, derive VM.
165    /// 2. Is `<name>` a VM? Load `vm/<name>/target.toml`.
166    /// 3. Neither? Error.
167    pub fn resolve(name: &str) -> Result<Self, Diagnostic> {
168        // 1. Try OS
169        if let Some(os_config) = UnionConfig::resolve(name)? {
170            let vm = TerrainConfig::resolve(&os_config.vm)?;
171            return Ok(ResolvedTarget {
172                vm,
173                os: Some(os_config),
174                state: None,
175            });
176        }
177
178        // 2. Try VM
179        let vm = TerrainConfig::resolve(name)?;
180        Ok(ResolvedTarget {
181            vm,
182            os: None,
183            state: None,
184        })
185    }
186
187    /// Resolve a target with an explicit state.
188    ///
189    /// A state requires a union target — bare terrain (VM) targets
190    /// cannot have states because states are chain instances within
191    /// a network.
192    pub fn resolve_with_state(target: &str, state_name: Option<&str>) -> Result<Self, Diagnostic> {
193        let mut resolved = Self::resolve(target)?;
194        if let Some(state) = state_name {
195            let union = resolved
196                .os
197                .as_ref()
198                .map(|os| os.name.as_str())
199                .ok_or_else(|| {
200                    Diagnostic::error(
201                        format!(
202                            "--state requires a union target, not bare terrain '{}'",
203                            target
204                        ),
205                        Span::dummy(),
206                    )
207                })?;
208            resolved.state = StateConfig::resolve(union, state)?;
209            if resolved.state.is_none() {
210                return Err(Diagnostic::error(
211                    format!("unknown state '{}' for union '{}'", state, union),
212                    Span::dummy(),
213                )
214                .with_help(format!(
215                    "available states: {}",
216                    StateConfig::list_states(union).join(", ")
217                )));
218            }
219        }
220        Ok(resolved)
221    }
222}
223
224/// Parse a minimal TOML string array: `["a", "b", "c"]` → `vec!["a", "b", "c"]`.
225pub fn parse_string_array(s: &str) -> Vec<String> {
226    let s = s.trim();
227    if !s.starts_with('[') || !s.ends_with(']') {
228        return Vec::new();
229    }
230    let inner = &s[1..s.len() - 1];
231    inner
232        .split(',')
233        .map(|part| part.trim().trim_matches('"').to_string())
234        .filter(|s| !s.is_empty())
235        .collect()
236}