Skip to main content

nucleus_compiler/
config.rs

1//! Parsing of the `stm32.toml` project configuration.
2//!
3//! The format is documented in the project README. This module owns only the
4//! *syntactic* layer: turn TOML text into a typed [`Config`]. It does no
5//! hardware validation — that is the solver's job (see [`crate::solver`]).
6//!
7//! Peripheral tables carry a mix of pin assignments (`tx = "PA2"`) and tuning
8//! parameters (`baud = 115200`), and the set of keys differs per peripheral
9//! kind. Rather than hard-code every peripheral struct here, each
10//! `[peripherals.<name>]` table is kept as a raw key→value map and interpreted
11//! by the solver against the [`crate::model`] role tables. This keeps the
12//! parser stable as new peripherals are added.
13
14use std::collections::BTreeMap;
15
16use serde::Deserialize;
17
18/// A parsed `stm32.toml`.
19#[derive(Debug, Clone, Default, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct Config {
22    #[serde(default)]
23    pub device: Device,
24    #[serde(default)]
25    pub build: Build,
26    /// `[peripherals.<instance>]` tables, keyed by instance name as written
27    /// (e.g. `"usart2"`). Order is normalized to lexical for deterministic
28    /// diagnostics.
29    #[serde(default)]
30    pub peripherals: BTreeMap<String, Peripheral>,
31    /// `[clocks]` — which device bus domains are enabled. Absent means "all
32    /// enabled" so a config that omits the section never trips the clock check.
33    #[serde(default)]
34    pub clocks: Clocks,
35    #[serde(default)]
36    pub trace: Trace,
37}
38
39/// The `[device]` section.
40#[derive(Debug, Clone, Default, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct Device {
43    #[serde(default)]
44    pub family: String,
45    #[serde(default)]
46    pub board: Option<String>,
47    #[serde(default)]
48    pub clock_hz: Option<u64>,
49}
50
51/// The `[build]` section.
52#[derive(Debug, Clone, Default, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct Build {
55    #[serde(default)]
56    pub toolchain: Option<String>,
57    #[serde(default)]
58    pub optimization: Option<String>,
59}
60
61/// One `[peripherals.<instance>]` table: a raw bag of keys. Pin roles are
62/// string values; tuning parameters are everything else.
63#[derive(Debug, Clone, Default, Deserialize)]
64#[serde(transparent)]
65pub struct Peripheral(pub BTreeMap<String, toml::Value>);
66
67impl Peripheral {
68    /// The string value of `key`, if present and a string (i.e. a pin role).
69    pub fn pin_str(&self, key: &str) -> Option<&str> {
70        self.0.get(key).and_then(toml::Value::as_str)
71    }
72}
73
74/// The `[clocks]` section. Each field defaults to `true` (enabled) so that an
75/// omitted field never produces a false "clock disabled" error.
76#[derive(Debug, Clone, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct Clocks {
79    #[serde(default = "enabled")]
80    pub ahb1: bool,
81    #[serde(default = "enabled")]
82    pub apb1: bool,
83    #[serde(default = "enabled")]
84    pub apb2: bool,
85}
86
87fn enabled() -> bool {
88    true
89}
90
91impl Default for Clocks {
92    fn default() -> Clocks {
93        Clocks {
94            ahb1: true,
95            apb1: true,
96            apb2: true,
97        }
98    }
99}
100
101/// The `[trace]` section. Parsed for completeness; not validated in Phase 2.
102#[derive(Debug, Clone, Default, Deserialize)]
103#[serde(deny_unknown_fields)]
104pub struct Trace {
105    #[serde(default)]
106    pub enabled: bool,
107    #[serde(default)]
108    pub swo_freq: Option<u64>,
109    #[serde(default)]
110    pub variables: Vec<TraceVariable>,
111}
112
113/// One `[[trace.variables]]` entry.
114#[derive(Debug, Clone, Default, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct TraceVariable {
117    pub name: String,
118    pub port: u8,
119    #[serde(rename = "type")]
120    pub ty: String,
121}
122
123/// Error returned when `stm32.toml` text is not valid TOML or violates the schema.
124#[derive(Debug)]
125pub struct ParseError(toml::de::Error);
126
127impl ParseError {
128    /// The byte span in the source text the error refers to, if the underlying
129    /// TOML parser recorded one. Used by the LSP to place a diagnostic range.
130    pub fn span(&self) -> Option<std::ops::Range<usize>> {
131        self.0.span()
132    }
133
134    /// The bare error message (without the `invalid stm32.toml:` prefix).
135    pub fn message(&self) -> &str {
136        self.0.message()
137    }
138}
139
140impl std::fmt::Display for ParseError {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "invalid stm32.toml: {}", self.0.message())
143    }
144}
145
146impl std::error::Error for ParseError {}
147
148/// Parse `stm32.toml` text into a [`Config`].
149pub fn parse(text: &str) -> Result<Config, ParseError> {
150    toml::from_str(text).map_err(ParseError)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn parses_readme_example() {
159        let cfg = parse(
160            r#"
161[device]
162family = "STM32F446RE"
163board = "NUCLEO-F446RE"
164clock_hz = 180_000_000
165
166[peripherals.usart2]
167tx = "PA2"
168rx = "PA3"
169baud = 115200
170
171[peripherals.spi1]
172mosi = "PA7"
173miso = "PA6"
174sck = "PA5"
175nss = "PA4"
176mode = 0
177"#,
178        )
179        .unwrap();
180
181        assert_eq!(cfg.device.family, "STM32F446RE");
182        assert_eq!(cfg.peripherals.len(), 2);
183        assert_eq!(cfg.peripherals["usart2"].pin_str("tx"), Some("PA2"));
184        // Tuning params are not pin roles.
185        assert_eq!(cfg.peripherals["spi1"].pin_str("mode"), None);
186        // Clocks default to all-enabled.
187        assert!(cfg.clocks.apb1 && cfg.clocks.apb2 && cfg.clocks.ahb1);
188    }
189
190    #[test]
191    fn rejects_unknown_top_level_section() {
192        assert!(parse("[nonsense]\nfoo = 1\n").is_err());
193    }
194
195    #[test]
196    fn clocks_section_can_disable_a_bus() {
197        let cfg = parse("[clocks]\napb1 = false\n").unwrap();
198        assert!(!cfg.clocks.apb1);
199        assert!(cfg.clocks.apb2); // unspecified -> enabled
200    }
201}