Skip to main content

comnum_spec/blocks/
registry.rs

1//! The registry of the blocks: their documentation ([`BlockInfo`]), the families of
2//! this crate, the blocks of other crates ([`register`]) and the builder ([`build`]).
3//!
4//! Every block, of this crate or of another one, is an [`Extension`]: its documentation
5//! and its builder, which receives its parameters (without `type` and `label`), its
6//! label and the context.
7
8use super::per_pol::PerPol;
9use super::schema::{self, Fact, ParamDefault, ParamInfo};
10use super::{Block, BuildContext};
11use super::{analysis, ce_ofdm, channel, coding, cpm, dsp, fiber, optical, source, systems, wireless};
12use crate::params::split_block;
13use comnum_core::{Error, Result};
14use serde_json::{Map, Value as Json};
15use std::fmt::Write;
16use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
17
18/// The documentation of a block ([`BlockInfo`](crate::blocks::BlockInfo)) from its
19/// parameters, declared with [`block_params!`](crate::block_params), and its ports.
20///
21/// ```text
22/// block_info!([start | per_polarization] NAME, SUMMARY, PARAMS, needs [FACT, …], gives [FACT, …])
23/// ```
24///
25/// `start` marks a block that starts a link (it draws or reads the transmitted signal),
26/// `per_polarization` a block written for one polarisation, applied to each
27/// polarisation of a dual-polarisation channel with its own symbols; the facts are
28/// variants of [`Fact`](crate::blocks::Fact). See [`register`](crate::blocks::register)
29/// for a block of another crate.
30#[macro_export]
31macro_rules! block_info {
32    (@info $starts:expr, $per_pol:expr, $name:expr, $summary:expr, $params:ty,
33        needs [$($n:ident),* $(,)?], gives [$($g:ident),* $(,)?] $(,)?) => {
34        $crate::blocks::BlockInfo {
35            name: $name,
36            summary: $summary,
37            params: <$params as $crate::blocks::Params>::params(),
38            needs: &[$($crate::blocks::Fact::$n),*],
39            gives: &[$($crate::blocks::Fact::$g),*],
40            starts: $starts,
41            per_polarization: $per_pol,
42        }
43    };
44    (start $($rest:tt)+) => { $crate::block_info!(@info true, false, $($rest)+) };
45    (per_polarization $($rest:tt)+) => { $crate::block_info!(@info false, true, $($rest)+) };
46    ($($rest:tt)+) => { $crate::block_info!(@info false, false, $($rest)+) };
47}
48
49/// A block of this crate as an [`Extension`]: the arguments of
50/// [`block_info!`](crate::block_info), then its builder
51/// `|params, label, ctx| -> Result<Box<dyn Block>>`, which receives the parameters
52/// parsed with their `block_params!` declaration (the name of the block in the errors).
53macro_rules! builtin {
54    ($($flag:ident)? $name:literal, $summary:expr, $params:ty,
55        needs [$($n:ident),* $(,)?], gives [$($g:ident),* $(,)?],
56        |$q:pat_param, $label:pat_param, $ctx:pat_param| $body:expr $(,)?) => {
57        $crate::blocks::Extension {
58            info: block_info!($($flag)? $name, $summary, $params, needs [$($n),*], gives [$($g),*]),
59            build: |p, label, ctx| {
60                let build = |$q: $params,
61                             $label: String,
62                             $ctx: &$crate::blocks::BuildContext|
63                 -> comnum_core::Result<Box<dyn $crate::blocks::Block>> { $body };
64                build(<$params as $crate::blocks::Params>::parse($name, p)?, label, ctx)
65            },
66        }
67    };
68}
69
70/// The documentation of a block: its parameters and its ports.
71#[derive(Clone, Debug)]
72pub struct BlockInfo {
73    /// The type of the block.
74    pub name: &'static str,
75    /// What it does.
76    pub summary: &'static str,
77    /// Its parameters.
78    pub params: Vec<ParamInfo>,
79    /// The facts of the frame it reads.
80    pub needs: &'static [Fact],
81    /// The facts it may add to the frame.
82    pub gives: &'static [Fact],
83    /// Whether it starts a link (it draws or reads the transmitted signal).
84    pub starts: bool,
85    /// Whether it is written for one polarisation: with a dual-polarisation channel
86    /// ([`Layout::polarisations`](super::Layout::polarisations)) each polarisation goes
87    /// through it with its own symbols.
88    pub per_polarization: bool,
89}
90
91impl BlockInfo {
92    /// The block as text (name, summary, ports, one line per parameter).
93    pub fn describe(&self) -> String {
94        let names = |f: &[Fact]| {
95            if f.is_empty() { "nothing".to_string() } else { f.iter().map(|f| f.name()).collect::<Vec<_>>().join(", ") }
96        };
97        let mut s = format!("{}\n    {}\n", self.name, self.summary);
98        let start = if self.starts { "starts a link; " } else { "" };
99        let _ = writeln!(s, "    [{start}needs: {}; gives: {}]", names(self.needs), names(self.gives));
100        for p in &self.params {
101            s.push_str(&p.describe(4));
102        }
103        s
104    }
105
106    /// The block as JSON: name, summary, ports and the JSON Schema of every parameter.
107    pub fn to_json(&self) -> Json {
108        let params: Map<String, Json> = self.params.iter().map(|p| (p.name.to_string(), p.json_schema())).collect();
109        serde_json::json!({
110            "name": self.name,
111            "summary": self.summary,
112            "starts": self.starts,
113            "per_polarization": self.per_polarization,
114            "needs": self.needs.iter().map(|f| f.name()).collect::<Vec<_>>(),
115            "gives": self.gives.iter().map(|f| f.name()).collect::<Vec<_>>(),
116            "required": self.params.iter().filter(|p| matches!(p.default, ParamDefault::Required)).map(|p| p.name).collect::<Vec<_>>(),
117            "params": params,
118        })
119    }
120}
121
122/// The blocks of a family of this crate.
123type FamilyFn = fn() -> Vec<Extension>;
124
125/// The families of blocks of this crate, in the order of the registry.
126const FAMILIES: &[FamilyFn] = &[
127    source::blocks,
128    channel::blocks,
129    fiber::blocks,
130    dsp::blocks,
131    ce_ofdm::blocks,
132    cpm::blocks,
133    analysis::blocks,
134    optical::blocks,
135    wireless::blocks,
136    coding::blocks,
137    systems::blocks,
138];
139
140/// The blocks of this crate, built once.
141fn builtins() -> &'static [Extension] {
142    static BUILTINS: OnceLock<Vec<Extension>> = OnceLock::new();
143    BUILTINS.get_or_init(|| FAMILIES.iter().flat_map(|family| family()).collect())
144}
145
146/// A block of the registry, added by [`register`] for the blocks of another crate.
147///
148/// `build` receives the parameters of the block (without `type` and `label`), its
149/// label and the context; the blocks of this crate are built the same way.
150#[derive(Clone)]
151pub struct Extension {
152    /// The documentation of the block.
153    pub info: BlockInfo,
154    /// Its builder.
155    pub build: fn(Json, String, &BuildContext) -> Result<Box<dyn Block>>,
156}
157
158static EXTENSIONS: Mutex<Vec<Extension>> = Mutex::new(Vec::new());
159
160/// The blocks added by [`register`], locked.
161fn extensions() -> MutexGuard<'static, Vec<Extension>> {
162    EXTENSIONS.lock().unwrap_or_else(PoisonError::into_inner)
163}
164
165/// Add a block to the registry: it is listed by [`registry`], built by [`build`],
166/// described by [`json_schema`] and checked by [`check_link`] like the others. A crate
167/// that defines blocks registers them before it runs a specification, and may ship its
168/// own `comnum` binary around [`cli::main`](crate::cli::main).
169///
170/// # Errors
171/// [`Error::Config`] when a block of that name exists.
172///
173/// # Examples
174///
175/// A block of another crate: its parameters are declared with
176/// [`block_params!`](crate::block_params) and its ports with
177/// [`block_info!`](crate::block_info), as those of the blocks of this crate, so that
178/// `comnum blocks`, the JSON Schema and the checks of the links describe it too.
179///
180/// ```
181/// use comnum_core::rng::Rng;
182/// use comnum_spec::blocks::{self, Block, BuildContext, Extension, Frame, Params};
183/// use comnum_spec::{RunOptions, Spec, block_info, block_params, run};
184/// use serde_json::Value as Json;
185///
186/// block_params! {
187///     struct GainSpec {
188///         gain_db: f64 [0.0] "dB", "power gain";
189///     }
190/// }
191///
192/// /// Multiply every sample by an amplitude gain.
193/// struct Gain(f64);
194///
195/// impl Block for Gain {
196///     fn apply(&self, s: &mut Frame, _: &mut Rng) -> comnum_core::Result<()> {
197///         s.streams_mut().flatten().for_each(|v| *v *= self.0);
198///         Ok(())
199///     }
200/// }
201///
202/// fn build(p: Json, _label: String, _: &BuildContext) -> comnum_core::Result<Box<dyn Block>> {
203///     let q = GainSpec::parse("gain", p)?;
204///     Ok(Box::new(Gain(comnum_math::db2amp(q.gain_db))))
205/// }
206///
207/// blocks::register(Extension {
208///     info: block_info!("gain", "Amplify the samples.", GainSpec, needs [Samples], gives []),
209///     build,
210/// })?;
211/// let spec = Spec::from_yaml(
212///     "name: amplified\nlink: [source, {type: gain, gain_db: 6}]\n\
213///      metrics: {report: [ser]}\nstop: {max_trials: 2}",
214/// )?;
215/// let results = run(&spec, &RunOptions::default())?;
216/// assert_eq!(results[0].metric("ser").unwrap().value(), 0.0);
217/// // listed with its parameters, and checked as the blocks of this crate
218/// let info = blocks::registry().into_iter().find(|b| b.name == "gain").unwrap();
219/// assert_eq!((info.params[0].name, info.params[0].unit), ("gain_db", "dB"));
220/// let unknown = serde_json::json!({"type": "gain", "gain": 6});
221/// assert!(blocks::build(&unknown, &BuildContext::default()).is_err());
222/// # Ok::<(), comnum_core::Error>(())
223/// ```
224pub fn register(ext: Extension) -> Result<()> {
225    let name = ext.info.name;
226    let mut extensions = extensions();
227    if builtins().iter().chain(extensions.iter()).any(|e| e.info.name == name) {
228        return Err(Error::Config(format!("a block {name:?} is already registered")));
229    }
230    extensions.push(ext);
231    Ok(())
232}
233
234/// Every block of the registry, in the order of a link: the blocks of this crate, then
235/// those added by [`register`].
236pub fn registry() -> Vec<BlockInfo> {
237    with_registry(|all| all.iter().map(|&b| b.clone()).collect())
238}
239
240/// Run `f` on every block of the registry, in its order, without copying them (the
241/// blocks of other crates locked meanwhile: `f` must not register a block).
242pub(crate) fn with_registry<R>(f: impl FnOnce(&[&BlockInfo]) -> R) -> R {
243    let extensions = extensions();
244    let all: Vec<&BlockInfo> = builtins().iter().chain(extensions.iter()).map(|e| &e.info).collect();
245    f(&all)
246}
247
248/// Build a block from its JSON description.
249///
250/// # Errors
251/// [`Error::Config`] for an unknown type (the message lists the known ones), an unknown
252/// or ill-typed parameter, or an invalid value.
253pub fn build(v: &Json, ctx: &BuildContext) -> Result<Box<dyn Block>> {
254    let (kind, label, p) = split_block(v)?;
255    build_parts(kind, label, p, ctx)
256}
257
258/// Build a block from its type, its label (its type by default) and its parameters.
259///
260/// # Errors
261/// As [`build`].
262#[cfg_attr(
263    not(debug_assertions),
264    allow(clippy::needless_pass_by_value, reason = "`kind` is consumed by the port check of the debug builds")
265)]
266pub(crate) fn build_parts(kind: String, label: Option<String>, p: Json, ctx: &BuildContext) -> Result<Box<dyn Block>> {
267    let label = label.unwrap_or_else(|| kind.clone());
268    // the lock of the blocks of other crates is released before the builder runs
269    let found = builtins()
270        .iter()
271        .find(|e| e.info.name == kind)
272        .map(|e| (e.build, e.info.gives, e.info.per_polarization))
273        .or_else(|| {
274            extensions().iter().find(|e| e.info.name == kind).map(|e| (e.build, e.info.gives, e.info.per_polarization))
275        });
276    let Some((builder, gives, per_polarization)) = found else {
277        let known: Vec<&str> = with_registry(|r| r.iter().map(|b| b.name).collect());
278        return Err(crate::unknown("block", &kind, &known));
279    };
280    let block = builder(p, label, ctx)?;
281    let block: Box<dyn Block> = if per_polarization { Box::new(PerPol(block)) } else { block };
282    // debug builds check the declared ports of every block while it runs
283    #[cfg(debug_assertions)]
284    let block: Box<dyn Block> = Box::new(schema::Checked { inner: block, kind, gives });
285    #[cfg(not(debug_assertions))]
286    let _ = gives;
287    Ok(block)
288}
289
290/// Check the ports of a link of block types (see [`schema::check_link`]).
291///
292/// # Errors
293/// [`Error::Config`] naming the first block that misses a fact.
294pub fn check_link(kinds: &[&str]) -> Result<()> {
295    with_registry(|r| schema::check_link(kinds, r))
296}
297
298/// The JSON Schema of a block of a link (see [`schema::json_schema`]).
299pub fn json_schema() -> Json {
300    with_registry(|r| schema::json_schema(r))
301}