Skip to main content

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    /// Output paths for sub-decoder instruction newtypes.
136    /// Maps sub-decoder name -> output file path.
137    /// Supports `$VAR` expansion (e.g. `$OUT_DIR/dsp_ext_instr.rs`).
138    #[serde(default)]
139    pub subdecoder_instr_type_outputs: HashMap<String, String>,
140
141    /// Sub-decoder instruction types: sub-decoder name -> Rust type path.
142    /// When set, the generated dispatch function takes this type instead of raw `u8`/`u16`.
143    #[serde(default)]
144    pub subdecoder_instr_types: HashMap<String, String>,
145}
146
147/// Dispatch strategy for code generation.
148///
149/// Controls how decoders, sub-decoders, and emulator LUTs dispatch to handlers.
150/// The names are language-neutral; each backend maps them to the appropriate
151/// language construct.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
153#[serde(rename_all = "snake_case")]
154pub enum Dispatch {
155    /// `#[inline(always)]` match statement (Rust), `switch` (C++), etc.
156    JumpTable,
157    /// Static function pointer lookup table.
158    #[default]
159    FnPtrLut,
160}
161
162/// Load a `ChipiConfig` from a TOML file.
163pub fn load_config(path: &Path) -> Result<ChipiConfig, ConfigError> {
164    let content =
165        std::fs::read_to_string(path).map_err(|e| ConfigError::Io(path.to_path_buf(), e))?;
166    let config: ChipiConfig =
167        toml::from_str(&content).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?;
168    Ok(config)
169}
170
171/// Expand environment variables (`$VAR` or `${VAR}`) in a string.
172/// Unresolved variables are left as-is.
173fn expand_env(s: &str) -> String {
174    let mut result = String::with_capacity(s.len());
175    let mut chars = s.chars().peekable();
176    while let Some(c) = chars.next() {
177        if c == '$' {
178            let braced = chars.peek() == Some(&'{');
179            if braced {
180                chars.next();
181            }
182            let mut name = String::new();
183            if braced {
184                while let Some(&c) = chars.peek() {
185                    if c == '}' {
186                        chars.next();
187                        break;
188                    }
189                    name.push(c);
190                    chars.next();
191                }
192            } else {
193                while let Some(&c) = chars.peek() {
194                    if c.is_ascii_alphanumeric() || c == '_' {
195                        name.push(c);
196                        chars.next();
197                    } else {
198                        break;
199                    }
200                }
201            }
202            if let Ok(val) = std::env::var(&name) {
203                result.push_str(&val);
204            } else if braced {
205                result.push_str(&format!("${{{}}}", name));
206            } else {
207                result.push('$');
208                result.push_str(&name);
209            }
210        } else {
211            result.push(c);
212        }
213    }
214    result
215}
216
217/// Resolve a path: expand env vars, then make relative paths relative to base_dir.
218fn resolve_path(path: &str, base_dir: &Path) -> String {
219    let expanded = expand_env(path);
220    let p = Path::new(&expanded);
221    if p.is_absolute() {
222        expanded
223    } else {
224        base_dir.join(&expanded).to_string_lossy().into_owned()
225    }
226}
227
228/// Resolve paths in a `GenTarget` relative to a base directory.
229/// Supports `$OUT_DIR`, `$CARGO_MANIFEST_DIR`, etc. in paths.
230pub fn resolve_gen_paths(target: &mut GenTarget, base_dir: &Path) {
231    target.input = resolve_path(&target.input, base_dir);
232    target.output = resolve_path(&target.output, base_dir);
233}
234
235/// Resolve paths in a `LutTarget` relative to a base directory.
236/// Supports `$OUT_DIR`, `$CARGO_MANIFEST_DIR`, etc. in paths.
237pub fn resolve_lut_paths(target: &mut LutTarget, base_dir: &Path) {
238    target.input = resolve_path(&target.input, base_dir);
239    target.output = resolve_path(&target.output, base_dir);
240    if let Some(ref mut p) = target.instr_type_output {
241        *p = resolve_path(p, base_dir);
242    }
243    for p in target.subdecoder_instr_type_outputs.values_mut() {
244        *p = resolve_path(p, base_dir);
245    }
246}
247
248#[derive(Debug)]
249pub enum ConfigError {
250    Io(PathBuf, std::io::Error),
251    Parse(PathBuf, toml::de::Error),
252}
253
254impl std::fmt::Display for ConfigError {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            ConfigError::Io(path, e) => write!(f, "failed to read {}: {}", path.display(), e),
258            ConfigError::Parse(path, e) => {
259                write!(f, "failed to parse {}: {}", path.display(), e)
260            }
261        }
262    }
263}
264
265impl std::error::Error for ConfigError {}