Skip to main content

nucleus_compiler/
lib.rs

1//! The Nucleus pinmux compiler.
2//!
3//! Owns the `stm32.toml` → diagnostics pipeline: [`config`] parses the file,
4//! [`solver`] validates it against the [`nucleus_db`] constraint database, and
5//! [`check`] is the one-call entry point the CLI and (later) the LSP build on.
6//!
7//! Phase 2 ships the parser and the constraint solver (four conflict classes).
8//! HAL code generation lands in Phase 3.
9
10pub mod codegen;
11pub mod config;
12pub mod model;
13pub mod solver;
14
15use nucleus_db::Database;
16
17pub use codegen::{generate, Generated};
18pub use config::{Config, ParseError};
19pub use solver::Conflict;
20
21/// The outcome of checking one `stm32.toml`.
22#[derive(Debug, Clone)]
23pub struct CheckReport {
24    /// The parsed config (useful to callers that go on to codegen).
25    pub config: Config,
26    /// All detected conflicts, in deterministic order. Empty means the config
27    /// is valid.
28    pub conflicts: Vec<Conflict>,
29}
30
31impl CheckReport {
32    /// Whether the config is free of conflicts.
33    pub fn is_ok(&self) -> bool {
34        self.conflicts.is_empty()
35    }
36}
37
38/// The database to validate against for `family`. Empty or `"STM32F446RE"`
39/// resolves to the F446RE; `"STM32F411RE"` to the F411RE. Any other value is an
40/// [`UnknownFamily`] error so the CLI/LSP can warn and fall back.
41pub fn database_for(family: &str) -> Result<Database, UnknownFamily> {
42    match family {
43        "STM32F446RE" | "" => Ok(Database::f446re()),
44        "STM32F411RE" => Ok(Database::f411re()),
45        other => Err(UnknownFamily(other.to_string())),
46    }
47}
48
49/// Returned when `[device].family` names an MCU the database doesn't cover yet.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct UnknownFamily(pub String);
52
53impl std::fmt::Display for UnknownFamily {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(
56            f,
57            "unsupported device family {:?}: Nucleus supports STM32F446RE and STM32F411RE",
58            self.0
59        )
60    }
61}
62
63impl std::error::Error for UnknownFamily {}
64
65/// Parse and validate `stm32.toml` text in one call.
66///
67/// Returns [`ParseError`] only for malformed TOML / schema violations; hardware
68/// conflicts are returned *inside* the [`CheckReport`] (a valid file can still
69/// describe an invalid board).
70pub fn check(text: &str) -> Result<CheckReport, ParseError> {
71    let config = config::parse(text)?;
72    // An unknown family is itself a conflict-worthy condition, but we model it
73    // as falling back to the F446 DB; the CLI surfaces the family mismatch.
74    let db = database_for(&config.device.family).unwrap_or_else(|_| Database::f446re());
75    let conflicts = solver::solve(&config, &db);
76    Ok(CheckReport { config, conflicts })
77}
78
79/// Like [`check`], but also reports an unsupported `[device].family`.
80pub fn check_family(text: &str) -> Result<(CheckReport, Option<UnknownFamily>), ParseError> {
81    let config = config::parse(text)?;
82    let family_warning = database_for(&config.device.family).err();
83    let db = database_for(&config.device.family).unwrap_or_else(|_| Database::f446re());
84    let conflicts = solver::solve(&config, &db);
85    Ok((CheckReport { config, conflicts }, family_warning))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn check_reports_ok_for_clean_config() {
94        let report = check(
95            r#"
96[device]
97family = "STM32F446RE"
98
99[peripherals.usart2]
100tx = "PA2"
101rx = "PA3"
102"#,
103        )
104        .unwrap();
105        assert!(report.is_ok());
106    }
107
108    #[test]
109    fn check_surfaces_conflicts() {
110        let report = check(
111            r#"
112[peripherals.spi1]
113mosi = "PA7"
114miso = "PA6"
115sck = "PA5"
116
117[peripherals.tim2]
118channel1 = "PA5"
119"#,
120        )
121        .unwrap();
122        assert!(!report.is_ok());
123    }
124
125    #[test]
126    fn unknown_family_is_flagged() {
127        let (_report, warning) = check_family(
128            r#"
129[device]
130family = "STM32H750"
131"#,
132        )
133        .unwrap();
134        assert_eq!(warning, Some(UnknownFamily("STM32H750".to_string())));
135    }
136
137    #[test]
138    fn malformed_toml_is_a_parse_error() {
139        assert!(check("this is not toml = = =").is_err());
140    }
141
142    #[test]
143    fn database_for_resolves_known_families() {
144        assert!(database_for("STM32F446RE").is_ok());
145        assert!(database_for("STM32F411RE").is_ok());
146        assert!(database_for("").is_ok()); // empty falls back to F446RE
147        assert!(database_for("STM32H750").is_err());
148    }
149
150    #[test]
151    fn check_family_resolves_db_for_f411re() {
152        // UART4 is absent on the F411RE; check_family must validate against the
153        // F411 DB (not the F446 fallback) and report it, with no family warning
154        // since STM32F411RE is a recognized family.
155        let (report, warning) = check_family(
156            "[device]\nfamily = \"STM32F411RE\"\n\n[peripherals.uart4]\ntx = \"PA0\"\nrx = \"PA1\"\n",
157        )
158        .unwrap();
159        assert_eq!(warning, None);
160        assert!(
161            report.conflicts.iter().any(|c| matches!(
162                c,
163                Conflict::PeripheralUnavailable { peripheral, family }
164                    if peripheral == "UART4" && family == "STM32F411RE"
165            )),
166            "got {:?}",
167            report.conflicts
168        );
169    }
170}