chipi_core/config.rs
1//! Configuration types for chipi code generation.
2//!
3//! Defines the TOML config schema (`chipi.toml`) and the target types
4//! that carry all settings for code generation runs.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11/// Top-level chipi.toml configuration.
12///
13/// A single config file can define multiple generation targets of both kinds.
14#[derive(Debug, Clone, Default, Deserialize, Serialize)]
15pub struct ChipiConfig {
16 /// Decoder/disassembler generation targets.
17 #[serde(rename = "gen", default)]
18 pub targets: Vec<GenTarget>,
19
20 /// Emulator dispatch LUT generation targets.
21 #[serde(default)]
22 pub lut: Vec<LutTarget>,
23}
24
25/// A single decoder/disassembler code generation target.
26#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct GenTarget {
28 /// Path to the input `.chipi` file.
29 /// Relative paths are resolved from the TOML file's directory.
30 /// Supports `$VAR` / `${VAR}` environment variable expansion (e.g. `$OUT_DIR`).
31 pub input: String,
32
33 /// Target language backend. Currently only `"rust"` is supported.
34 pub lang: String,
35
36 /// Output file path.
37 /// Relative paths are resolved from the TOML file's directory.
38 /// Supports `$VAR` / `${VAR}` environment variable expansion (e.g. `$OUT_DIR`).
39 pub output: String,
40
41 /// Whether to run a language-appropriate formatter on the output.
42 #[serde(default)]
43 pub format: bool,
44
45 /// Default dispatch strategy for all decoders/sub-decoders.
46 #[serde(default)]
47 pub dispatch: Dispatch,
48
49 /// Per-decoder dispatch strategy overrides.
50 #[serde(default)]
51 pub dispatch_overrides: HashMap<String, Dispatch>,
52
53 /// Type mappings: chipi type name -> language-specific type path.
54 #[serde(default)]
55 pub type_map: HashMap<String, String>,
56
57 /// Reserved for language-specific settings.
58 #[serde(default)]
59 pub lang_options: Option<toml::Value>,
60}
61
62impl GenTarget {
63 /// Create a new `GenTarget` with the given input, lang, and output.
64 pub fn new(
65 input: impl Into<String>,
66 lang: impl Into<String>,
67 output: impl Into<String>,
68 ) -> Self {
69 Self {
70 input: input.into(),
71 lang: lang.into(),
72 output: output.into(),
73 format: false,
74 dispatch: Dispatch::default(),
75 dispatch_overrides: HashMap::new(),
76 type_map: HashMap::new(),
77 lang_options: None,
78 }
79 }
80}
81
82/// A single emulator dispatch LUT generation target.
83#[derive(Debug, Clone, Deserialize, Serialize)]
84pub struct LutTarget {
85 /// Path to the input `.chipi` file.
86 /// Supports `$VAR` expansion and relative paths (resolved from the TOML file's directory).
87 pub input: String,
88
89 /// Output file path for the LUT dispatch code.
90 /// Supports `$VAR` expansion (e.g. `$OUT_DIR/lut.rs`).
91 pub output: String,
92
93 /// Rust module path where handler functions live.
94 pub handler_mod: String,
95
96 /// Mutable context type passed to every handler.
97 pub ctx_type: String,
98
99 /// Dispatch strategy.
100 #[serde(default)]
101 pub dispatch: Dispatch,
102
103 /// Instruction groups: group name -> list of instruction names.
104 /// Instructions in a group share one const-generic handler function.
105 #[serde(default)]
106 pub groups: HashMap<String, Vec<String>>,
107
108 /// Rust module path where the generated OP_* constants live.
109 /// Required when using groups so stubs can import the constants.
110 #[serde(default)]
111 pub lut_mod: Option<String>,
112
113 /// Override the type of the second handler parameter (default: width-derived u8/u16/u32).
114 /// Set to a wrapper type like `"crate::cpu::Instruction"`.
115 #[serde(default)]
116 pub instr_type: Option<String>,
117
118 /// Expression to extract the raw integer from the instr local.
119 /// Default: `"instr.0"` when `instr_type` is set, `"opcode"` otherwise.
120 #[serde(default)]
121 pub raw_expr: Option<String>,
122
123 /// Output file path for the instruction newtype with field accessors.
124 /// Supports `$VAR` expansion.
125 /// If set, generates a `pub struct Name(pub u32)` with accessor methods.
126 #[serde(default)]
127 pub instr_type_output: Option<String>,
128
129 /// Sub-decoder groups: sub-decoder name -> { group name -> [instruction names] }.
130 /// Instructions in a group share one const-generic handler function.
131 /// If a sub-decoder is listed here, a `dispatch_{snake_name}` function is generated.
132 #[serde(default)]
133 pub subdecoder_groups: HashMap<String, HashMap<String, Vec<String>>>,
134}
135
136/// Dispatch strategy for code generation.
137///
138/// Controls how decoders, sub-decoders, and emulator LUTs dispatch to handlers.
139/// The names are language-neutral; each backend maps them to the appropriate
140/// language construct.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
142#[serde(rename_all = "snake_case")]
143pub enum Dispatch {
144 /// `#[inline(always)]` match statement (Rust), `switch` (C++), etc.
145 JumpTable,
146 /// Static function pointer lookup table.
147 #[default]
148 FnPtrLut,
149}
150
151/// Load a `ChipiConfig` from a TOML file.
152pub fn load_config(path: &Path) -> Result<ChipiConfig, ConfigError> {
153 let content =
154 std::fs::read_to_string(path).map_err(|e| ConfigError::Io(path.to_path_buf(), e))?;
155 let config: ChipiConfig =
156 toml::from_str(&content).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?;
157 Ok(config)
158}
159
160/// Expand environment variables (`$VAR` or `${VAR}`) in a string.
161/// Unresolved variables are left as-is.
162fn expand_env(s: &str) -> String {
163 let mut result = String::with_capacity(s.len());
164 let mut chars = s.chars().peekable();
165 while let Some(c) = chars.next() {
166 if c == '$' {
167 let braced = chars.peek() == Some(&'{');
168 if braced {
169 chars.next();
170 }
171 let mut name = String::new();
172 if braced {
173 while let Some(&c) = chars.peek() {
174 if c == '}' {
175 chars.next();
176 break;
177 }
178 name.push(c);
179 chars.next();
180 }
181 } else {
182 while let Some(&c) = chars.peek() {
183 if c.is_ascii_alphanumeric() || c == '_' {
184 name.push(c);
185 chars.next();
186 } else {
187 break;
188 }
189 }
190 }
191 if let Ok(val) = std::env::var(&name) {
192 result.push_str(&val);
193 } else if braced {
194 result.push_str(&format!("${{{}}}", name));
195 } else {
196 result.push('$');
197 result.push_str(&name);
198 }
199 } else {
200 result.push(c);
201 }
202 }
203 result
204}
205
206/// Resolve a path: expand env vars, then make relative paths relative to base_dir.
207fn resolve_path(path: &str, base_dir: &Path) -> String {
208 let expanded = expand_env(path);
209 let p = Path::new(&expanded);
210 if p.is_absolute() {
211 expanded
212 } else {
213 base_dir.join(&expanded).to_string_lossy().into_owned()
214 }
215}
216
217/// Resolve paths in a `GenTarget` relative to a base directory.
218/// Supports `$OUT_DIR`, `$CARGO_MANIFEST_DIR`, etc. in paths.
219pub fn resolve_gen_paths(target: &mut GenTarget, base_dir: &Path) {
220 target.input = resolve_path(&target.input, base_dir);
221 target.output = resolve_path(&target.output, base_dir);
222}
223
224/// Resolve paths in a `LutTarget` relative to a base directory.
225/// Supports `$OUT_DIR`, `$CARGO_MANIFEST_DIR`, etc. in paths.
226pub fn resolve_lut_paths(target: &mut LutTarget, base_dir: &Path) {
227 target.input = resolve_path(&target.input, base_dir);
228 target.output = resolve_path(&target.output, base_dir);
229 if let Some(ref mut p) = target.instr_type_output {
230 *p = resolve_path(p, base_dir);
231 }
232}
233
234#[derive(Debug)]
235pub enum ConfigError {
236 Io(PathBuf, std::io::Error),
237 Parse(PathBuf, toml::de::Error),
238}
239
240impl std::fmt::Display for ConfigError {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match self {
243 ConfigError::Io(path, e) => write!(f, "failed to read {}: {}", path.display(), e),
244 ConfigError::Parse(path, e) => {
245 write!(f, "failed to parse {}: {}", path.display(), e)
246 }
247 }
248 }
249}
250
251impl std::error::Error for ConfigError {}