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}