consortium-cfg 0.5.0

Configuration subsystem support for the Consortium project.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
// Copyright 2026 Ethan Wu
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//! Debug-logging IR: one defmt-over-shared-memory ring per coprocessor.
//!
//! A logging core's generated `init()` hands its resolved ring to
//! `ConsortiumLogger::init` ([`DebugRing::initialize_controller`]); the AP's
//! generated `init()` maps every ring to read it ([`DebugLogs::initialize_host`]).
//! Generated code is the only place the layout is written down; there are no
//! build-time `CONSORTIUM_DBG_*` variables.

use std::collections::HashMap;
use std::path::PathBuf;

use consortium_cfg_common::config::{DbgMode, VisibleCore};
use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote};
use syn::Ident;

/// One coprocessor's debug log ring, resolved to the address its firmware embeds
/// at compile time plus the host-side mapping the AP uses to read it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebugRing {
    /// Physical base of the ring as seen from the logging core's bus, i.e. the
    /// manifest base plus the chip's SRAM alias for that core's world.
    pub base: u64,
    /// Ring length in bytes.
    pub length: u64,
    /// Maximum transfer unit for debug frames.
    pub mtu: u32,
    /// UIO device index the AP maps this ring through (the aggregate `dbg-shm`
    /// device).
    pub ap_uio_id: usize,
    /// mmap window index of this ring within that device.
    pub ap_mmap_id: usize,
    /// Build-host location of this core's compiled firmware ELF, from which the
    /// host resolves the defmt symbol table.
    ///
    /// [`Session::lower`](crate::Session::lower) seeds this with the manifest's
    /// `[endpoints]` value (a source-directory placeholder). The build pipeline
    /// then overwrites it via [`DebugLogs::with_resolved_elf_paths`] with the
    /// firmware ELF it just compiled, so the AP codegen can `include_bytes!` the
    /// real image at compile time.
    pub elf: ElfLocation,
}

/// Where the AP codegen finds a debug core's firmware ELF.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElfLocation {
    /// Compile-time environment variable naming the directory [`path`](Self::path)
    /// is relative to (e.g. `CSTI_MCU_PATH`), or `None` to embed `path` verbatim.
    pub base_env: Option<String>,
    /// The ELF path: relative to `base_env` when set, otherwise used as-is.
    pub path: PathBuf,
}

impl ElfLocation {
    /// A path the generated `include_bytes!` names verbatim.
    pub fn verbatim(path: impl Into<PathBuf>) -> Self {
        Self {
            base_env: None,
            path: path.into(),
        }
    }

    /// A path relative to the directory the `base_env` variable holds when the
    /// generated module is compiled, so the module carries no build-host path.
    pub fn relative_to(base_env: impl Into<String>, path: impl Into<PathBuf>) -> Self {
        Self {
            base_env: Some(base_env.into()),
            path: path.into(),
        }
    }

    /// The `include_bytes!` argument: a string literal, or
    /// `concat!(env!("<VAR>"), "/<path>")` when relative to a variable.
    fn to_tokens(&self) -> TokenStream {
        // Normalize separators: a Windows build host would otherwise emit `\`
        // escapes that are not portable into the generated source.
        let path = self.path.to_string_lossy().replace('\\', "/");
        match &self.base_env {
            None => quote! { #path },
            Some(var) => {
                let suffix = format!("/{}", path.trim_start_matches("./"));
                quote! { ::core::concat!(::core::env!(#var), #suffix) }
            }
        }
    }
}

impl DebugRing {
    /// The logging core's statement that resets this ring and starts logging into
    /// it. It must run before the first `defmt` frame.
    ///
    /// The descriptor stores the length as a `u16`. A longer ring is a
    /// `compile_error!` in the generated module rather than a silently truncated
    /// ring.
    pub fn initialize_controller(&self) -> TokenStream {
        let base = proc_macro2::Literal::usize_suffixed(self.base as usize);
        match u16::try_from(self.length) {
            Ok(len) => {
                let len = proc_macro2::Literal::u16_suffixed(len);
                quote! {
                    /// SAFETY: the ring is this core's validated `[dbg]` window,
                    /// written only by this logger and read by the host.
                    unsafe { ::consortium_dbg::logger::ConsortiumLogger::init((#base) as *mut u8, #len) }
                }
            }
            Err(_) => {
                let message = format!(
                    "the [dbg] ring is {} bytes, but a debug descriptor holds at most {} bytes",
                    self.length,
                    u16::MAX
                );
                quote! { ::core::compile_error!(#message); }
            }
        }
    }
}

/// Every coprocessor that declares a `[dbg.<core>]` ring, keyed by logging core,
/// plus the host consumption [`DbgMode`] selected by `[dbg] mode`.
#[derive(Debug, Clone, Default)]
pub struct DebugLogs {
    rings: HashMap<VisibleCore, DebugRing>,
    mode: DbgMode,
}

impl DebugLogs {
    /// Builds the IR from a resolved per-core ring map and the host mode.
    pub fn new(rings: HashMap<VisibleCore, DebugRing>, mode: DbgMode) -> Self {
        Self { rings, mode }
    }

    /// Every resolved logger, keyed by the coprocessor it runs on.
    pub fn rings(&self) -> &HashMap<VisibleCore, DebugRing> {
        &self.rings
    }

    /// The host consumption mode for the rings.
    pub fn mode(&self) -> DbgMode {
        self.mode
    }

    /// The ring configured for `core`, or `None` when it has no debug logging.
    pub fn ring(&self, core: VisibleCore) -> Option<DebugRing> {
        self.rings.get(&core).cloned()
    }

    /// Overwrites each ring's [`elf`](DebugRing::elf) with the location of that
    /// core's compiled firmware ELF.
    ///
    /// [`Session::lower`](crate::Session::lower) seeds `elf_path` with the
    /// manifest's `[endpoints]` value (a source directory, not an artifact), so
    /// the build pipeline calls this once Stage 1 has produced the real ELFs,
    /// right before generating the AP codegen that `include_bytes!`es them. A
    /// ring whose core is absent from `resolved` keeps its seeded value; the
    /// pipeline's `resolve_dbg_elf_paths` guarantees every `[dbg.<core>]` ring is
    /// present, so a leftover placeholder means a bug upstream.
    pub fn with_resolved_elf_paths(mut self, resolved: &HashMap<VisibleCore, ElfLocation>) -> Self {
        for (core, ring) in self.rings.iter_mut() {
            if let Some(elf) = resolved.get(core) {
                ring.elf = elf.clone();
            }
        }
        self
    }

    /// Generates the AP-side host console code, or `None` when no coprocessor
    /// declares a debug ring.
    ///
    /// The returned [`HostCodegen`] carries the platform-resource field
    /// (declared *before* `uio` so its threads/handles drop before the mappings
    /// they read are unmapped) and the `init()` statement that brings it up. In
    /// [`DbgMode::Auto`] this spawns a forwarding console; in
    /// [`DbgMode::Interface`] it returns reader handles for the integrator.
    pub fn initialize_host(&self, uio_ident: &Ident) -> Option<HostCodegen> {
        if self.rings.is_empty() {
            return None;
        }

        // Deterministic order: sort by core name so the generated specs and the
        // dbg-shm mmap windows agree run to run.
        let mut rings: Vec<(&VisibleCore, &DebugRing)> = self.rings.iter().collect();
        rings.sort_by_key(|(core, _)| core.to_string());

        let specs = rings.iter().map(|(core, ring)| {
            let uio_id = Literal::usize_unsuffixed(ring.ap_uio_id);
            let mmap_id = Literal::usize_unsuffixed(ring.ap_mmap_id);
            let len = Literal::usize_unsuffixed(ring.length as usize);
            // The builder resolves the ELF relative to a `CSTI_*` directory
            // variable it injects into the application build (see
            // [`DebugLogs::with_resolved_elf_paths`] and the pipeline's
            // `resolve_dbg_elf_paths`), so `include_bytes!` resolves the same
            // image regardless of where the generated file lands and the module
            // carries no build-host path. Codegen never touches the filesystem:
            // the ELF need not exist yet (the builder compiles it in a later
            // stage; tests point at a fixture relative to the emitted crate).
            let elf_path = ring.elf.to_tokens();
            let label = core.to_string();
            // `include_bytes!` yields a byte array with alignment 1, but
            // `defmt-decoder`'s ELF parser (`object`) requires the image to be
            // aligned to its natural boundary. Re-home the embedded bytes into a
            // 16-aligned `static` and hand out a slice into it so the alignment
            // survives into `DefmtContext::from_elf_bytes`.
            quote! {
                ::consortium_runtime_app::dbg_console::DbgRingSpec {
                    uio_id: #uio_id,
                    mmap_id: #mmap_id,
                    len: #len,
                    elf_bytes: {
                        #[repr(align(16))]
                        struct Aligned<const N: usize>([u8; N]);
                        static ELF: Aligned<{ include_bytes!(#elf_path).len() }> =
                            Aligned(*include_bytes!(#elf_path));
                        &ELF.0
                    },
                    label: #label,
                }
            }
        });
        let specs = quote! { &[ #(#specs),* ] };

        let field_name = format_ident!("dbg_console");
        let (field, init) = match self.mode {
            DbgMode::Auto => (
                quote! {
                    pub #field_name: ::core::option::Option<
                        ::consortium_runtime_app::dbg_console::DbgConsole
                    >
                },
                quote! {
                    let #field_name = if config.dbg_console {
                        ::core::option::Option::Some(
                            ::consortium_runtime_app::dbg_console::spawn(&#uio_ident, #specs)?
                        )
                    } else {
                        ::core::option::Option::None
                    };
                },
            ),
            DbgMode::Interface => (
                quote! {
                    pub #field_name: ::core::option::Option<
                        ::std::vec::Vec<::consortium_runtime_app::dbg_console::DbgRingHandle>
                    >
                },
                quote! {
                    let #field_name = if config.dbg_console {
                        ::core::option::Option::Some(
                            ::consortium_runtime_app::dbg_console::open(&#uio_ident, #specs)?
                        )
                    } else {
                        ::core::option::Option::None
                    };
                },
            ),
        };

        Some(HostCodegen {
            field,
            field_name,
            init,
        })
    }
}

/// The fragments [`DebugLogs::initialize_host`] contributes to the AP's
/// generated platform `Peripherals` and `init(config)`.
pub struct HostCodegen {
    /// Platform `Peripherals` field declaration (`name: Type`).
    pub field: TokenStream,
    /// The field's identifier, for the struct literal in `init()`.
    pub field_name: Ident,
    /// The `let <field_name> = …;` statement that brings the console up.
    pub init: TokenStream,
}

#[cfg(test)]
mod tests {
    use super::*;
    use consortium_cfg_common::config::McoreName;

    fn cm33() -> VisibleCore {
        VisibleCore::Mcore(McoreName::Cm33)
    }

    fn ring() -> DebugRing {
        DebugRing {
            base: 0x2040_8000,
            length: 0x2000,
            mtu: 64,
            ap_uio_id: 3,
            ap_mmap_id: 0,
            elf: ElfLocation::verbatim("./firmware"),
        }
    }

    #[test]
    fn controller_init_passes_the_ring_to_the_logger() {
        let tokens = ring().initialize_controller().to_string();
        assert!(
            tokens.contains(
                ":: consortium_dbg :: logger :: ConsortiumLogger :: init ((541097984usize) as * mut u8 , 8192u16)"
            ),
            "{tokens}"
        );
    }

    #[test]
    fn a_ring_too_long_for_the_descriptor_is_a_compile_error() {
        let mut ring = ring();
        ring.length = 0x1_0000;
        let tokens = ring.initialize_controller().to_string();
        assert!(tokens.contains("compile_error"), "{tokens}");
        assert!(!tokens.contains("ConsortiumLogger"), "{tokens}");
    }

    #[test]
    fn logs_key_rings_by_core() {
        let logs = DebugLogs::new(HashMap::from([(cm33(), ring())]), DbgMode::Auto);

        assert!(logs.ring(cm33()).is_some());
        assert!(logs.ring(VisibleCore::Ap).is_none());
    }

    #[test]
    fn host_codegen_none_when_empty() {
        let logs = DebugLogs::new(HashMap::new(), DbgMode::Auto);
        let uio = format_ident!("uio");
        assert!(logs.initialize_host(&uio).is_none());
    }

    #[test]
    fn auto_mode_spawns_console_with_ring_spec() {
        let logs = DebugLogs::new(HashMap::from([(cm33(), ring())]), DbgMode::Auto);
        let uio = format_ident!("uio");
        let host = logs.initialize_host(&uio).expect("rings present");

        let init_str = host.init.to_string();
        assert!(init_str.contains(":: consortium_runtime_app :: dbg_console :: spawn"));
        assert!(init_str.contains("DbgRingSpec"));
        assert!(init_str.contains("\"cm33\""));
        // The firmware ELF is embedded at compile time, not read from a path, so
        // the spec carries `include_bytes!("<path>")` (re-homed into a 16-aligned
        // static for `elf_bytes`).
        assert!(init_str.contains("include_bytes ! (\"./firmware\")"));
        assert!(!init_str.contains("elf_path"));
        assert!(host.field.to_string().contains("DbgConsole"));

        // The init statement parses as a valid Rust block.
        let init = &host.init;
        let wrapped = quote! { fn _f(uio: ()) -> ::core::result::Result<(), ()> { #init Ok(()) } };
        syn::parse_file(&wrapped.to_string()).expect("host init is valid Rust");
    }

    #[test]
    fn with_resolved_elf_paths_embeds_the_build_host_path() {
        // The pipeline substitutes each ring's seeded `[endpoints]` placeholder
        // with the firmware ELF it compiled; a verbatim location makes the
        // generated `include_bytes!` name exactly that path.
        let logs = DebugLogs::new(HashMap::from([(cm33(), ring())]), DbgMode::Auto);
        let elf = ElfLocation::verbatim("/build/target/thumbv8m.main-none-eabihf/release/firmware");
        let logs = logs.with_resolved_elf_paths(&HashMap::from([(cm33(), elf.clone())]));

        assert_eq!(logs.ring(cm33()).expect("cm33 ring").elf, elf);

        let uio = format_ident!("uio");
        let host = logs.initialize_host(&uio).expect("rings present");
        let init_str = host.init.to_string();
        assert!(init_str.contains(
            "include_bytes ! (\"/build/target/thumbv8m.main-none-eabihf/release/firmware\")"
        ));

        // The generated spec still parses as valid Rust.
        let init = &host.init;
        let wrapped = quote! { fn _f(uio: ()) -> ::core::result::Result<(), ()> { #init Ok(()) } };
        syn::parse_file(&wrapped.to_string()).expect("host init is valid Rust");
    }

    #[test]
    fn relative_elf_location_resolves_through_env() {
        // The builder's usual shape: the ELF path relative to a `CSTI_*` directory
        // injected into the application build, so no build-host path is emitted.
        let logs = DebugLogs::new(HashMap::from([(cm33(), ring())]), DbgMode::Auto);
        let elf = ElfLocation::relative_to(
            "CSTI_MCU_PATH",
            "target/thumbv8m.main-none-eabihf/release/firmware_original.elf",
        );
        let logs = logs.with_resolved_elf_paths(&HashMap::from([(cm33(), elf)]));

        let uio = format_ident!("uio");
        let host = logs.initialize_host(&uio).expect("rings present");
        let init_str = host.init.to_string();
        assert!(init_str.contains(
            "include_bytes ! (:: core :: concat ! (:: core :: env ! (\"CSTI_MCU_PATH\") , \"/target/thumbv8m.main-none-eabihf/release/firmware_original.elf\"))"
        ));

        let init = &host.init;
        let wrapped = quote! { fn _f(uio: ()) -> ::core::result::Result<(), ()> { #init Ok(()) } };
        syn::parse_file(&wrapped.to_string()).expect("host init is valid Rust");
    }

    #[test]
    fn interface_mode_opens_reader_handles() {
        let logs = DebugLogs::new(HashMap::from([(cm33(), ring())]), DbgMode::Interface);
        let uio = format_ident!("uio");
        let host = logs.initialize_host(&uio).expect("rings present");

        assert!(
            host.init
                .to_string()
                .contains(":: consortium_runtime_app :: dbg_console :: open")
        );
        assert!(host.field.to_string().contains("DbgRingHandle"));
    }
}