Skip to main content

nros_params/
server.rs

1//! Parameter server implementation
2//!
3//! Provides a static storage parameter server for embedded systems.
4//! Parameters are stored in a fixed-size array with compile-time capacity.
5
6use crate::{
7    ParameterError,
8    types::{
9        FloatingPointRange, IntegerRange, MAX_PARAM_NAME_LEN, NodeFlags, NodeKey, Parameter,
10        ParameterDescriptor, ParameterRange, ParameterType, ParameterValue,
11    },
12};
13use heapless::String;
14
15// MAX_PARAMETERS is generated by build.rs and included via types.rs.
16use crate::types::MAX_PARAMETERS;
17
18/// phase-491 W3 (RFC-0105 D8) — what a pending `/parameter_events` entry
19/// reports. The value is read from the store when the event is published, so
20/// an event says what the parameter IS, not what it was when it moved.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ParameterEventKind {
23    /// Declared (locally, or by a remote set on an undeclared-allowing node).
24    New,
25    /// Its value changed.
26    Changed,
27    /// Undeclared.
28    Deleted,
29    /// Issue 2023 — the node's start-up event when it holds no parameter at
30    /// the moment events go on: the node's name and three EMPTY lists. A
31    /// node that does hold parameters announces itself with one `New` per
32    /// parameter instead (see [`ParameterServer::pop_initial_parameter_event`](crate::ParameterServerOps::pop_initial_parameter_event)).
33    Announce,
34}
35
36/// One change waiting to be published on `/parameter_events`.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PendingParameterEvent {
39    /// The node whose parameter moved.
40    pub node: NodeKey,
41    /// What happened to it.
42    pub kind: ParameterEventKind,
43    /// Its name.
44    pub name: String<MAX_PARAM_NAME_LEN>,
45}
46
47/// How many changes the store holds between two flushes of
48/// `/parameter_events`.
49///
50/// A flush runs after every C or C++ parameter call and on every spin, so this only
51/// has to cover the changes ONE spin's parameter services make; a burst past
52/// it is counted ([`ParameterServer::take_parameter_events_dropped`](crate::ParameterServerOps::take_parameter_events_dropped)) and
53/// reported, never blocked on. A second change to a name already pending
54/// merges into its entry, since the event carries the value at publish time.
55pub const PARAMETER_EVENT_LOG_LEN: usize = 8;
56
57/// Entry in the parameter storage.
58///
59/// phase-382 W2' — crate-private ON PURPOSE. The storage is caller-owned now,
60/// but the SLOT layout is not part of the deal: consumers name
61/// [`ParameterTable`] / [`ParameterStorage`], never this.
62#[derive(Debug, Clone)]
63pub(crate) struct ParameterEntry {
64    /// phase-426 W1 — which node declared it. The slot arena is shared (one
65    /// fixed table per executor, no per-node allocation); this is what keeps
66    /// two nodes' identically-named parameters apart inside it.
67    node: NodeKey,
68    /// The parameter (name + value)
69    param: Parameter,
70    /// Optional descriptor with constraints
71    descriptor: Option<ParameterDescriptor>,
72}
73
74/// Why a declaration was refused — phase-417 W4.a.
75///
76/// rclrs separates DECLARE-time failures from set-time ones, and this is that
77/// separation: a write's `Result<(), ParameterError>` answers "may this WRITE happen", this
78/// answers "may this parameter EXIST". Ours is a subset of rclrs's, and the
79/// difference is structural rather than an omission: rclrs's
80/// `NoValueAvailable`, `OverrideValueTypeMismatch` and `PriorValueTypeMismatch`
81/// are about a launch OVERRIDE arriving before the declaration and disagreeing
82/// with it, and nano-ros seeds overrides through the same
83/// [`ParameterServer::apply`](crate::ParameterServerOps::apply) path a remote set uses — so a disagreeing
84/// override is a `ParameterError`, on the write that carried it, and
85/// cannot reach a declaration.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum DeclarationError {
88    /// This node already declared this name (rclrs `AlreadyDeclared`).
89    AlreadyDeclared,
90    /// The descriptor's range is ill-formed — `min > max`, or a negative step
91    /// (rclrs `InvalidRange`).
92    InvalidRange,
93    /// The default value is outside the descriptor's range (rclrs
94    /// `InitialValueOutOfRange`).
95    InitialValueOutOfRange,
96    /// Every slot of the caller-owned table is taken. No rclrs correspondent:
97    /// rclrs's store grows, ours is the storage the caller placed.
98    StorageFull,
99    /// The name is longer than `NROS_MAX_PARAM_NAME_LEN`. No rclrs
100    /// correspondent, same reason.
101    NameTooLong,
102    /// A read-only parameter was declared with no default (rclrs
103    /// `NoValueAvailable`; phase-515 R8). A read-only value can never be set
104    /// later, so it must arrive with the declaration.
105    NoValueAvailable,
106}
107
108/// phase-515 R8 -- the typed builder's terminals return [`DeclarationError`]
109/// now, as rclrs's do. This keeps `?` into a [`ParameterError`]-returning
110/// caller compiling, and maps each case to the `ParameterError` such a caller
111/// saw before the change.
112impl From<DeclarationError> for crate::ParameterError {
113    fn from(e: DeclarationError) -> Self {
114        match e {
115            DeclarationError::AlreadyDeclared => crate::ParameterError::TypeMismatch,
116            DeclarationError::InvalidRange => crate::ParameterError::InvalidRange,
117            DeclarationError::InitialValueOutOfRange => crate::ParameterError::OutOfRange,
118            DeclarationError::StorageFull | DeclarationError::NameTooLong => {
119                crate::ParameterError::StorageFull
120            }
121            DeclarationError::NoValueAvailable => crate::ParameterError::NotFound,
122        }
123    }
124}
125
126/// An on-set-parameters callback — phase-417 W4.a.
127///
128/// `false` REFUSES the write, which is rclcpp's
129/// `add_on_set_parameters_callback` contract (a `SetParametersResult` whose
130/// `successful` is false) and the contract C's legacy
131/// C's rclc parameter server carries too (phase-491, with the old value).
132///
133/// A `fn` pointer plus an [`OnSetContext`], not a closure: this crate is
134/// `no_std` with no allocator, so there is nothing to box a `FnMut` into.
135///
136/// phase-491 W2 (RFC-0105 D6) — the hook sees BOTH sides of the change,
137/// rclc's `rclc_parameter_callback_t` contract: `old` is `None` when the write
138/// would CREATE the parameter (an undeclared remote set), `new` is `None` when
139/// it would DELETE it (a remote `NOT_SET` on a node that asked for
140/// [`set_delete_on_not_set`](crate::ParameterServerOps::set_delete_on_not_set)).
141/// rclcpp's `add_on_set_parameters_callback` reads only `new`, so the C++ and
142/// Rust facades ignore `old`.
143pub type OnSetParameterFn = fn(
144    node: NodeKey,
145    name: &str,
146    old: Option<&ParameterValue>,
147    new: Option<&ParameterValue>,
148    ctx: OnSetContext,
149) -> bool;
150
151/// Two machine words of caller context, carried verbatim to an
152/// [`OnSetParameterFn`].
153///
154/// TWO, not one, because of the consumer that drives this API: a C or C++
155/// registration is a FUNCTION POINTER plus a user `void*`, and the shim needs
156/// both to reach the caller's code. One word would force each wrapper to keep
157/// a static side table mapping an index back to the pair — a second, smaller
158/// registry with its own bound and its own exhaustion story, which is the
159/// class phase-426 spent six work items removing.
160///
161/// `usize`, not `*mut c_void`: a raw pointer in this struct would strip
162/// [`ParameterServer`]'s auto traits, and the store lives inside the executor.
163/// The C and C++ shims cast through it and say so.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
165pub struct OnSetContext {
166    /// First word. The wrappers put the user's `void* context` here, so a
167    /// Rust caller with one pointer to carry uses this one and ignores `b`.
168    pub a: usize,
169    /// Second word. The wrappers put the C function pointer here.
170    pub b: usize,
171}
172
173impl OnSetContext {
174    /// One word of context; `b` is zero.
175    pub const fn new(a: usize) -> Self {
176        Self { a, b: 0 }
177    }
178
179    /// Both words.
180    pub const fn pair(a: usize, b: usize) -> Self {
181        Self { a, b }
182    }
183}
184
185/// A registered [`OnSetParameterFn`], returned by
186/// [`ParameterServer::add_on_set_parameters_callback`](crate::ParameterServerOps::add_on_set_parameters_callback) and consumed by
187/// [`ParameterServer::remove_on_set_parameters_callback`](crate::ParameterServerOps::remove_on_set_parameters_callback).
188///
189/// rclcpp hands back an owning `OnSetParametersCallbackHandle` shared_ptr whose
190/// destruction unregisters. With no allocator there is nothing to own, so this
191/// is a plain index token and unregistering is explicit.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct OnSetParameterHandle(u16);
194
195impl OnSetParameterHandle {
196    /// The token as a plain integer, for an FFI that must hand it to C.
197    ///
198    /// An opaque `u16` rather than a pointer: it indexes the bounded registry
199    /// and nothing else, so a wrong value is a `false` from
200    /// [`ParameterServer::remove_on_set_parameters_callback`](crate::ParameterServerOps::remove_on_set_parameters_callback) rather than a
201    /// wild write.
202    pub const fn raw(self) -> u16 {
203        self.0
204    }
205
206    /// Rebuild a handle a C caller kept. See [`Self::raw`].
207    pub const fn from_raw(raw: u16) -> Self {
208        Self(raw)
209    }
210}
211
212#[derive(Clone, Copy)]
213struct OnSetCallbackSlot {
214    node: NodeKey,
215    callback: OnSetParameterFn,
216    ctx: OnSetContext,
217}
218
219/// How many on-set-parameters callbacks ONE store holds, across every node.
220///
221/// phase-417 W4.a — a BOUNDED registry with a stated capacity, because the
222/// unbounded upstream shape needs an allocator this crate does not have. Four,
223/// and the number is chosen rather than inherited: a slot costs
224/// `size_of::<Option<OnSetCallbackSlot>>()` — 32 bytes on a 64-bit host, 16 on
225/// a 32-bit target — so the whole registry is 128 bytes next to a 285,184-byte
226/// default table, and four is one hook for each node of a four-node composed
227/// image (RFC-0047's shape) with none left to spare by accident. Registering a
228/// fifth REFUSES with `None` rather than dropping one silently.
229///
230/// It is deliberately NOT a build knob. Every other parameter capacity sizes
231/// the table a user's data lands in; this one sizes a table of code the image
232/// itself registers, which is known at build time by reading the source. A
233/// knob here would be a number nobody can derive and everybody has to state.
234pub const MAX_ON_SET_CALLBACKS: usize = 4;
235
236/// The slot table a [`ParameterServer`] borrows.
237///
238/// phase-382 W2' — the server's storage is CALLER-OWNED, and this newtype is
239/// what makes that expressible without publishing the slot type. A slot is a
240/// `ParameterEntry`, which stays PRIVATE (it is the store's internal slot
241/// layout, not an API); a consumer that only needs to *name* the storage —
242/// `nros-node`, which carves it out of the executor backing in W3' — names a
243/// `ParameterTable` instead.
244///
245/// **The table's LENGTH is the server's capacity.** [`MAX_PARAMETERS`] is the
246/// DEFAULT capacity (see [`ParameterStorage`]), not a bound the borrow
247/// re-imposes: a caller who hands over a longer or shorter table gets exactly
248/// that many slots, and every capacity question the server answers
249/// ([`is_full`](crate::ParameterServerOps::is_full), the declare path) reads it from
250/// here.
251pub struct ParameterTable<'s> {
252    entries: &'s mut [Option<ParameterEntry>],
253}
254
255impl<'s> ParameterTable<'s> {
256    /// Wrap a caller-owned slice of slots.
257    pub(crate) fn new(entries: &'s mut [Option<ParameterEntry>]) -> Self {
258        Self { entries }
259    }
260
261    /// Number of slots — the capacity of a server built on this table.
262    pub fn capacity(&self) -> usize {
263        self.entries.len()
264    }
265
266    /// Number of slots currently holding a parameter.
267    pub fn occupied(&self) -> usize {
268        self.entries.iter().filter(|slot| slot.is_some()).count()
269    }
270}
271
272/// One slot of a [`ParameterTable`], opaque.
273///
274/// phase-382 W3' — what a caller CARVES when it places a table in memory it
275/// already owns (the executor's backing). The slot layout is
276/// `Option<ParameterEntry>` and stays private; this wrapper publishes only its
277/// size and alignment (`size_of::<ParameterSlot>()`), which is exactly what a
278/// region placement needs, and [`ParameterTable::init_in`] is the one way to
279/// turn carved, uninitialised slots into a table.
280///
281/// `repr(transparent)`, so a `[ParameterSlot]` IS a `[Option<ParameterEntry>]`
282/// and the table borrows it without a copy.
283#[repr(transparent)]
284pub struct ParameterSlot(Option<ParameterEntry>);
285
286impl<'s> ParameterTable<'s> {
287    /// Initialise `slots` to empty IN PLACE and borrow them as a table.
288    ///
289    /// phase-382 W3' — the home the executor carves for its store. Each slot
290    /// is written through its own pointer, so the largest stack temporary is
291    /// one slot, never the table (issue 0756's hazard, which a carved region
292    /// avoids by construction as long as nothing builds the table by value).
293    /// Any length works; the length is the capacity, as for every table.
294    pub fn init_in(slots: &'s mut [core::mem::MaybeUninit<ParameterSlot>]) -> Self {
295        for slot in slots.iter_mut() {
296            slot.write(ParameterSlot(None));
297        }
298        let len = slots.len();
299        let base = slots.as_mut_ptr().cast::<Option<ParameterEntry>>();
300        // SAFETY: every element was written above, so the slice is initialised;
301        // `ParameterSlot` is `repr(transparent)` over `Option<ParameterEntry>`,
302        // so the element layouts are identical; the borrow is `slots`' own,
303        // for `'s`, and `slots` is not reachable again while it lives.
304        let entries = unsafe { core::slice::from_raw_parts_mut(base, len) };
305        Self::new(entries)
306    }
307}
308
309impl core::fmt::Debug for ParameterTable<'_> {
310    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
311        f.debug_struct("ParameterTable")
312            .field("capacity", &self.capacity())
313            .finish_non_exhaustive()
314    }
315}
316
317/// Backing storage a caller places, then lends to a [`ParameterServer`].
318///
319/// `N` defaults to [`MAX_PARAMETERS`], the build-time knob
320/// (`NROS_MAX_PARAMETERS` / `CONFIG_NROS_MAX_PARAMETERS`). It is a DEFAULT,
321/// not a cap — `ParameterStorage::<8>::new()` and `ParameterStorage::<256>`
322/// are equally valid, and the server takes its capacity from whatever table it
323/// is handed.
324///
325/// The intended shapes are a `static`, a caller-owned struct field, or (with
326/// `alloc`) one allocation made once at start-up:
327///
328/// ```
329/// use nros_params::{
330///     NodeKey, ParameterServer, ParameterServerOps as _, ParameterStorage, ParameterValue,
331/// };
332///
333/// let mut storage: ParameterStorage<8> = ParameterStorage::new();
334/// let mut server = ParameterServer::new_in(storage.as_table());
335/// assert!(server.declare(NodeKey::PRIMARY, "max_speed", ParameterValue::Double(1.0)));
336/// ```
337///
338/// # Why this type exists at all — issue 0756
339///
340/// This is where the size lives, and it is enormous: `ParameterValue` is sized
341/// by its largest variant
342/// (`StringArray(Vec<String<MAX_STRING_VALUE_LEN>, MAX_ARRAY_LEN>)`), so every
343/// slot costs ~8.5 KiB whatever it actually holds. Measured: **285,184 bytes**
344/// at the default 32 slots, **2,281,472** at 256. (The eight extra bytes in
345/// issue 0756's 285,192 were the `count` that stayed behind on
346/// [`ParameterServer`], which is 24 bytes total now.)
347///
348/// Before phase-382 W2' that bulk was INSIDE `ParameterServer`, so
349/// `Box::new(ParameterServer::new())` built a multi-hundred-KiB value on the
350/// STACK before copying it into the allocation — Rust has no placement-new —
351/// which silently overran a thread stack sized for anything smaller. On the
352/// Zephyr lane an image built with 256 slots booted to
353/// `dds_create_participant` and hung with no fault and no output, while 32 —
354/// which fits the 512 KiB main stack the cyclonedds snippet asks for — ran
355/// clean. The mitigation then was `ParameterServer::init_in_place`, a
356/// placement initialiser that bounded the largest temporary at one
357/// `Option<ParameterEntry>`.
358///
359/// W2' dissolves the *server* half of that: `ParameterServer` is now a table
360/// borrow plus a count (tens of bytes), so nothing about constructing a server
361/// depends on the knob. The bulk did not vanish, it MOVED here — and so did
362/// the hazard, verbatim: `Box::new(ParameterStorage::new())` is the same
363/// stack-first copy the issue was about. [`init_in_place`] is therefore kept,
364/// on the type that is actually big, and any heap-placed storage must go
365/// through it. A caller who places the storage as a `static` or lets W3' carve
366/// it out of the executor backing never materialises it by value and does not
367/// need it.
368///
369/// [`init_in_place`]: ParameterStorage::init_in_place
370pub struct ParameterStorage<const N: usize = MAX_PARAMETERS> {
371    entries: [Option<ParameterEntry>; N],
372}
373
374impl<const N: usize> ParameterStorage<N> {
375    /// Create empty storage.
376    ///
377    /// `const`, so a `static NROS_PARAMS: ParameterStorage = ParameterStorage::new();`
378    /// lands in `.bss` with no initialiser and no stack temporary.
379    // No `Default`: it would spell this same construction as a by-value
380    // expression, which is exactly the `Box::new(Default::default())` shape
381    // issue 0756 is about (see the type docs). Callers name `new()` — or
382    // `init_in_place` when the home is a heap allocation.
383    #[allow(clippy::new_without_default)]
384    #[allow(clippy::large_stack_arrays)] // Intentional: this IS the storage.
385    pub const fn new() -> Self {
386        Self {
387            entries: [const { None }; N],
388        }
389    }
390
391    /// Slots in this storage.
392    pub const fn capacity(&self) -> usize {
393        N
394    }
395
396    /// Lend the storage to a [`ParameterServer`].
397    pub fn as_table(&mut self) -> ParameterTable<'_> {
398        ParameterTable::new(&mut self.entries)
399    }
400
401    /// Initialise storage directly into `dst`, without ever materialising one
402    /// by value.
403    ///
404    /// Issue 0756 — see this type's documentation for why that matters. The
405    /// short version: this type is 285,184 bytes at the default 32 slots and
406    /// 2,281,472 at 256, and Rust has no placement-new, so
407    /// `Box::new(ParameterStorage::new())` builds all of it on the caller's
408    /// stack first. Initialising through the allocation bounds the largest
409    /// stack temporary at one `Option<ParameterEntry>`, so the
410    /// `NROS_MAX_PARAMETERS` knob no longer decides whether boot survives.
411    ///
412    /// # Safety
413    ///
414    /// `dst` must be non-null, correctly aligned, and valid for writes of
415    /// `size_of::<Self>()` bytes. The pointee may be uninitialised on entry;
416    /// it is fully initialised on return.
417    pub unsafe fn init_in_place(dst: *mut Self) {
418        // addr_of_mut! throughout: `(*dst).entries` as a place expression
419        // would require the pointee to already be initialised.
420        // Safety: delegated to this function's own contract on `dst`.
421        unsafe {
422            let entries = core::ptr::addr_of_mut!((*dst).entries).cast::<Option<ParameterEntry>>();
423            for i in 0..N {
424                entries.add(i).write(None);
425            }
426        }
427    }
428}
429
430/// Parameter server over caller-owned storage
431///
432/// Stores parameters in a [`ParameterTable`] the caller lends it; the table's
433/// length is the capacity. [`ParameterStorage`] is the usual way to produce
434/// one, defaulting to the build-time `MAX_PARAMETERS` slots.
435///
436/// # Keyed by node — phase-426 W1
437///
438/// Every per-parameter method takes a [`NodeKey`] first. The slot arena is
439/// shared (one table per executor), but a name is only ever looked up within
440/// one node, so two nodes composed onto one executor (RFC-0047) hold
441/// independent values for the same name — which is what upstream does, where
442/// each node owns a table and a set of six services.
443///
444/// # Example
445///
446/// ```
447/// use nros_params::{
448///     NodeKey, ParameterServer, ParameterServerOps as _, ParameterStorage, ParameterValue,
449/// };
450///
451/// let mut storage = ParameterStorage::<8>::new();
452/// let mut server = ParameterServer::new_in(storage.as_table());
453/// let talker = NodeKey::new(0);
454/// let listener = NodeKey::new(1);
455///
456/// // Declare a parameter with initial value
457/// server.declare(talker, "rate", ParameterValue::Double(1.0));
458/// server.declare(listener, "rate", ParameterValue::Double(2.0));
459///
460/// // Get parameter value
461/// assert_eq!(
462///     server.get(talker, "rate").and_then(|v| v.as_double()),
463///     Some(1.0)
464/// );
465/// assert_eq!(
466///     server.get(listener, "rate").and_then(|v| v.as_double()),
467///     Some(2.0)
468/// );
469///
470/// // Set parameter value
471/// server.set(talker, "rate", ParameterValue::Double(3.0));
472/// ```
473pub struct ParameterServer<'s> {
474    /// Borrowed parameter storage
475    table: ParameterTable<'s>,
476    /// Number of parameters currently stored, across every node.
477    count: usize,
478    /// May a wire-facing set ([`apply`](Self::apply)) DECLARE a name the node
479    /// does not hold? Off by default, as upstream
480    /// (`allow_undeclared_parameters` in rclcpp's `NodeOptions`,
481    /// `allow_undeclared` in rclrs) — issue 1151.
482    ///
483    /// phase-426 W1 — PER NODE, because upstream's is a node option. One node
484    /// opting in must not decide what a remote set may do to its sibling.
485    allow_undeclared: NodeFlags,
486    /// phase-491 W2 — nodes whose remote `NOT_SET` DELETES the parameter
487    /// (rclc's set-service behaviour) instead of being refused as a type
488    /// change (rclcpp's, for a statically typed parameter). Set by the C
489    /// `rclc_parameter_server_t`; per node because the two client libraries
490    /// disagree and both live in one store.
491    delete_on_not_set: NodeFlags,
492    /// phase-446 F2 -- a declared descriptor holds a truncated description
493    /// nobody has reported yet. Lets [`Self::take_truncated_descriptions`]
494    /// answer "nothing" without walking the table, which is what lets the
495    /// executor ask on every spin.
496    truncation_pending: bool,
497    /// phase-491 W3 — nodes whose changes are logged for `/parameter_events`.
498    events_enabled: NodeFlags,
499    /// phase-491 W3 — changes not yet published, oldest first.
500    event_log: heapless::Deque<PendingParameterEvent, PARAMETER_EVENT_LOG_LEN>,
501    /// phase-491 W3 — changes that found the log full since the last read.
502    events_dropped: u32,
503    /// Issue 2023 — nodes whose events just went ON and whose start-up
504    /// event (every parameter they already hold, as `New`) is not yet
505    /// drained. rclcpp's publisher exists before the first declaration, so
506    /// a subscriber sees every parameter arrive; ours goes on later (the
507    /// node table is built after the store), and this is what keeps the two
508    /// equal without a log entry per parameter.
509    events_initial: NodeFlags,
510    /// Issue 2023 — how far the start-up drain of [`Self::events_initial`]'s
511    /// lowest node has got, as an index into that node's parameters.
512    initial_cursor: usize,
513    /// phase-417 W4.a — the bounded on-set-parameters registry. See
514    /// [`MAX_ON_SET_CALLBACKS`] for why it is bounded and why 4.
515    on_set: [Option<OnSetCallbackSlot>; MAX_ON_SET_CALLBACKS],
516}
517
518impl<'s> ParameterServer<'s> {
519    /// phase-446 F2 -- report each declared parameter whose description was
520    /// truncated to `NROS_MAX_PARAM_DESCRIPTION_LEN`, ONCE, and forget it.
521    ///
522    /// This crate has no logger, so it records the truncation on the
523    /// descriptor and the executor, which has one, drains it here and logs.
524    /// The truncation itself happens where the text is given
525    /// ([`ParameterDescriptor::set_description`]), and a declaration can come
526    /// through several builders; the store is where every one of them lands.
527    /// Returns how many were reported.
528    pub(crate) fn take_truncated_descriptions(
529        &mut self,
530        mut report: impl FnMut(NodeKey, &str),
531    ) -> usize {
532        if !self.truncation_pending {
533            return 0;
534        }
535        self.truncation_pending = false;
536        let mut n = 0;
537        for entry in self.table.entries.iter_mut().flatten() {
538            if let Some(d) = entry.descriptor.as_mut()
539                && d.description_truncated()
540            {
541                d.clear_description_truncated();
542                report(entry.node, entry.param.name.as_str());
543                n += 1;
544            }
545        }
546        n
547    }
548
549    /// Note a descriptor entering the store (phase-446 F2).
550    #[inline]
551    fn note_descriptor(&mut self, descriptor: Option<&ParameterDescriptor>) {
552        if descriptor.is_some_and(ParameterDescriptor::description_truncated) {
553            self.truncation_pending = true;
554        }
555    }
556
557    /// Create a parameter server over `table`.
558    ///
559    /// The server ADOPTS whatever the table already holds: `count` is
560    /// recovered from the occupied slots rather than assumed zero. That is
561    /// what makes a table safe to hand to a second server after the first is
562    /// dropped (and what lets storage arrive pre-seeded), where assuming empty
563    /// would desynchronise the count from the slots — `is_full` wrong in one
564    /// direction, `remove` underflowing in the other. Fresh
565    /// [`ParameterStorage`] is all-`None`, so the usual path costs one scan
566    /// and starts at zero.
567    pub(crate) fn new_in(table: ParameterTable<'s>) -> Self {
568        let count = table.occupied();
569        Self {
570            table,
571            count,
572            allow_undeclared: NodeFlags::new(),
573            delete_on_not_set: NodeFlags::new(),
574            on_set: [None; MAX_ON_SET_CALLBACKS],
575            // An adopted table may already hold a truncated description, so
576            // the first drain looks rather than assuming none.
577            truncation_pending: true,
578            events_enabled: NodeFlags::new(),
579            event_log: heapless::Deque::new(),
580            events_dropped: 0,
581            events_initial: NodeFlags::new(),
582            initial_cursor: 0,
583        }
584    }
585
586    /// Let a wire-facing set ([`apply`](Self::apply)) create a parameter
587    /// `node` never declared. Default `false`: a remote set on a misspelled
588    /// name used to report success and create the misspelling (issue 1151);
589    /// now it is refused with a reason unless this is switched on, as
590    /// upstream.
591    ///
592    /// phase-426 W1 — scoped to `node`. Switching it on for `/talker` leaves
593    /// `/listener` refusing undeclared names.
594    pub(crate) fn set_allow_undeclared(&mut self, node: NodeKey, allow: bool) {
595        self.allow_undeclared.set(node, allow);
596    }
597
598    /// Whether [`apply`](Self::apply) may declare an unknown name on `node`.
599    pub(crate) fn allows_undeclared(&self, node: NodeKey) -> bool {
600        self.allow_undeclared.get(node)
601    }
602
603    /// phase-491 W3 — log `node`'s changes for `/parameter_events`
604    /// (rclc's `notify_changed_over_dds`). Off by default, so a node nobody
605    /// asked events for pays nothing.
606    ///
607    /// Issue 2023 — switching it ON (from off) also queues the node's
608    /// START-UP event: every parameter the node already holds is reported as
609    /// `New` by [`pop_initial_parameter_event`](Self::pop_initial_parameter_event),
610    /// or one [`ParameterEventKind::Announce`] when it holds none, so a
611    /// subscriber sees what rclcpp's would although our publisher comes up
612    /// after the first declarations.
613    pub(crate) fn set_parameter_events(&mut self, node: NodeKey, on: bool) {
614        if on && !self.events_enabled.get(node) {
615            self.events_initial.set(node, true);
616        }
617        if !on {
618            self.events_initial.set(node, false);
619            if self.events_initial.first().is_none() {
620                self.initial_cursor = 0;
621            }
622        }
623        self.events_enabled.set(node, on);
624    }
625
626    /// Whether `node`'s changes are logged for `/parameter_events`.
627    pub(crate) fn parameter_events_enabled(&self, node: NodeKey) -> bool {
628        self.events_enabled.get(node)
629    }
630
631    /// Issue 2023 — the next entry of a node's START-UP event, queued when its
632    /// events went on: one `New` per parameter it held then (read at drain
633    /// time, so it reports the current value), or one `Announce` for a node
634    /// that holds none. Drain this BEFORE [`pop_parameter_event`](Self::pop_parameter_event);
635    /// while a node's start-up event is pending its changes are not logged
636    /// separately, because the drain reports them.
637    ///
638    /// Bounded and allocation-free: a cursor into the fixed table, no copy of
639    /// the node's parameter list.
640    pub(crate) fn pop_initial_parameter_event(&mut self) -> Option<PendingParameterEvent> {
641        loop {
642            let node = self.events_initial.first()?;
643            let k = self.initial_cursor;
644            let next = self.iter(node).nth(k).map(|p| p.name.clone());
645            match next {
646                Some(name) => {
647                    self.initial_cursor = k + 1;
648                    return Some(PendingParameterEvent {
649                        node,
650                        kind: ParameterEventKind::New,
651                        name,
652                    });
653                }
654                None => {
655                    self.events_initial.set(node, false);
656                    self.initial_cursor = 0;
657                    if k == 0 {
658                        return Some(PendingParameterEvent {
659                            node,
660                            kind: ParameterEventKind::Announce,
661                            name: String::new(),
662                        });
663                    }
664                }
665            }
666        }
667    }
668
669    /// phase-491 W3 — the oldest change waiting for `/parameter_events`.
670    pub(crate) fn pop_parameter_event(&mut self) -> Option<PendingParameterEvent> {
671        self.event_log.pop_front()
672    }
673
674    /// phase-491 W3 — how many changes found the log full since the last
675    /// call, and reset the count.
676    pub(crate) fn take_parameter_events_dropped(&mut self) -> u32 {
677        core::mem::take(&mut self.events_dropped)
678    }
679
680    /// Record a change for `/parameter_events`, merging into an entry already
681    /// pending for the same name.
682    fn note_event(&mut self, node: NodeKey, kind: ParameterEventKind, name: &str) {
683        // Issue 2023 — a node whose start-up event is still pending is
684        // reported by that drain, which reads the table as it stands.
685        if !self.events_enabled.get(node) || self.events_initial.get(node) {
686            return;
687        }
688        if let Some(e) = self
689            .event_log
690            .iter_mut()
691            .find(|e| e.node == node && e.name.as_str() == name)
692        {
693            // New-then-changed is still new; anything then deleted is deleted;
694            // deleted-then-declared is new again.
695            e.kind = match (e.kind, kind) {
696                (ParameterEventKind::New, ParameterEventKind::Changed) => ParameterEventKind::New,
697                (_, k) => k,
698            };
699            return;
700        }
701        let mut stored = String::new();
702        if stored.push_str(name).is_err()
703            || self
704                .event_log
705                .push_back(PendingParameterEvent {
706                    node,
707                    kind,
708                    name: stored,
709                })
710                .is_err()
711        {
712            self.events_dropped = self.events_dropped.saturating_add(1);
713        }
714    }
715
716    /// phase-491 W2 — make a wire-facing `NOT_SET` on `node` DELETE the
717    /// parameter, after the on-set hooks accept `(old, None)`. This is rclc's
718    /// parameter server (`rclc_parameter_server_set_service_callback`); the
719    /// default, off, keeps rclcpp's refusal.
720    pub(crate) fn set_delete_on_not_set(&mut self, node: NodeKey, on: bool) {
721        self.delete_on_not_set.set(node, on);
722    }
723
724    /// Get the number of parameters `node` has stored.
725    pub(crate) fn len(&self, node: NodeKey) -> usize {
726        self.entries_of(node).count()
727    }
728
729    /// Check if `node` has no parameters
730    pub(crate) fn is_empty(&self, node: NodeKey) -> bool {
731        self.len(node) == 0
732    }
733
734    /// Parameters held across EVERY node — how much of the shared arena is in
735    /// use. [`len`](Self::len) is the per-node question; this one is the
736    /// capacity question's other half.
737    pub(crate) fn total_len(&self) -> usize {
738        self.count
739    }
740
741    /// Capacity of the borrowed table.
742    ///
743    /// phase-382 W2' — this is the TABLE's length, not `MAX_PARAMETERS`. The
744    /// build-time knob only picks the default size of a [`ParameterStorage`].
745    ///
746    /// phase-426 W1 — the arena is SHARED across nodes, so this is not a
747    /// per-node budget: `MAX_PARAMETERS` bounds the image, not each node.
748    pub(crate) fn capacity(&self) -> usize {
749        self.table.capacity()
750    }
751
752    /// Check if the server is at capacity (across every node)
753    pub(crate) fn is_full(&self) -> bool {
754        self.count >= self.table.capacity()
755    }
756
757    /// Occupied slots belonging to `node`.
758    fn entries_of(&self, node: NodeKey) -> impl Iterator<Item = &ParameterEntry> {
759        self.table
760            .entries
761            .iter()
762            .filter_map(|slot| slot.as_ref())
763            .filter(move |e| e.node == node)
764    }
765
766    /// Find the index of `node`'s parameter by name.
767    ///
768    /// phase-426 W1 — the node is part of the key, so a sibling's identically
769    /// named parameter is simply not found here.
770    fn find_index(&self, node: NodeKey, name: &str) -> Option<usize> {
771        self.table.entries.iter().position(|entry| {
772            entry
773                .as_ref()
774                .map(|e| e.node == node && e.param.name.as_str() == name)
775                .unwrap_or(false)
776        })
777    }
778
779    /// Find an empty slot
780    fn find_empty_slot(&self) -> Option<usize> {
781        self.table.entries.iter().position(|entry| entry.is_none())
782    }
783
784    /// Declare a new parameter with a value on `node`
785    ///
786    /// If the parameter already exists, this does nothing and returns false.
787    /// Returns true if the parameter was declared successfully.
788    ///
789    /// phase-428 W6 remainder: this is the method `Executor::declare_parameter`
790    /// delegates to, so marking only the wrapper would leave the class half
791    /// fixed one crate down.
792    #[must_use]
793    pub(crate) fn declare(&mut self, node: NodeKey, name: &str, value: ParameterValue) -> bool {
794        self.declare_with_descriptor(node, name, value, None)
795    }
796
797    /// [`declare`](Self::declare), borrowing the value (issue 2070): the
798    /// spelling a large value should take, since it is copied into its slot
799    /// once instead of through every frame.
800    #[must_use]
801    pub(crate) fn declare_ref(
802        &mut self,
803        node: NodeKey,
804        name: &str,
805        value: &ParameterValue,
806    ) -> bool {
807        self.try_declare_ref(node, name, value, None).is_ok()
808    }
809
810    /// Declare a new parameter with value and descriptor on `node`
811    ///
812    /// The descriptor provides metadata like description, constraints, and read-only flag.
813    ///
814    /// phase-428 W6 remainder: see [`Self::declare`]. Every `false` arm below
815    /// is a REFUSAL (name taken, table full, value rejected), never a question.
816    #[must_use]
817    pub(crate) fn declare_with_descriptor(
818        &mut self,
819        node: NodeKey,
820        name: &str,
821        value: ParameterValue,
822        descriptor: Option<ParameterDescriptor>,
823    ) -> bool {
824        self.try_declare_with_descriptor(node, name, value, descriptor)
825            .is_ok()
826    }
827
828    /// [`declare`](Self::declare), saying WHY it refused — phase-417 W4.a.
829    ///
830    /// rclrs separates declare-time failures from set-time ones
831    /// ([`DeclarationError`] against [`ParameterError`]); ours returned a
832    /// bare `bool` for "the name was taken", "the table was full" and "the
833    /// default is outside the range" alike, so a caller could not tell a
834    /// programming error from a capacity one. The `bool` spellings above
835    /// forward to this, so there is exactly one declare path.
836    pub(crate) fn try_declare(
837        &mut self,
838        node: NodeKey,
839        name: &str,
840        value: ParameterValue,
841    ) -> Result<(), DeclarationError> {
842        self.try_declare_with_descriptor(node, name, value, None)
843    }
844
845    /// [`declare_with_descriptor`](Self::declare_with_descriptor), saying WHY
846    /// it refused. See [`try_declare`](Self::try_declare).
847    pub(crate) fn try_declare_with_descriptor(
848        &mut self,
849        node: NodeKey,
850        name: &str,
851        value: ParameterValue,
852        descriptor: Option<ParameterDescriptor>,
853    ) -> Result<(), DeclarationError> {
854        self.try_declare_ref(node, name, &value, descriptor)
855    }
856
857    /// [`try_declare_with_descriptor`](Self::try_declare_with_descriptor),
858    /// borrowing the value — issue 2070. THE declare path; the by-value
859    /// spellings forward here.
860    ///
861    /// A [`ParameterValue`] is as large as its largest variant (`StringArray`:
862    /// `MAX_ARRAY_LEN` x `MAX_STRING_VALUE_LEN`, ~8 KiB at the defaults), and
863    /// the by-value path moved it through every frame and built it again in
864    /// [`Parameter::new`] and in the slot's `Some(..)`: ~57 KiB of stack for
865    /// one `declare_parameter<double>` on aarch64, ~67 KiB on Cortex-M3,
866    /// which overflowed a 64 KiB RTOS app stack into the kernel's globals.
867    /// Here the slot is filled with an empty value and the caller's is copied
868    /// into it once, in place ([`ParameterValue::assign_from`]).
869    pub(crate) fn try_declare_ref(
870        &mut self,
871        node: NodeKey,
872        name: &str,
873        value: &ParameterValue,
874        descriptor: Option<ParameterDescriptor>,
875    ) -> Result<(), DeclarationError> {
876        // Check if already exists
877        if self.find_index(node, name).is_some() {
878            return Err(DeclarationError::AlreadyDeclared);
879        }
880
881        // Issue 1150 — an ill-formed range, or a default the range rejects,
882        // is refused at declaration, as upstream. Same predicate as every set.
883        Self::declaration_consistency(descriptor.as_ref(), value)?;
884
885        // Find an empty slot
886        let slot = match self.find_empty_slot() {
887            Some(idx) => idx,
888            None => return Err(DeclarationError::StorageFull),
889        };
890
891        // The name is checked before the slot is touched, so a refusal
892        // leaves the table as it was.
893        let mut stored_name = String::new();
894        if stored_name.push_str(name).is_err() {
895            return Err(DeclarationError::NameTooLong);
896        }
897
898        self.note_descriptor(descriptor.as_ref());
899        // Issue 2070 -- the slot is written in place and the value copied in
900        // once: an entry built on the stack and moved in is a second ~8 KiB
901        // `ParameterValue` in this frame.
902        let cell = &mut self.table.entries[slot];
903        *cell = Some(ParameterEntry {
904            node,
905            param: Parameter {
906                name: stored_name,
907                value: ParameterValue::NotSet,
908            },
909            descriptor,
910        });
911        if let Some(entry) = cell {
912            entry.param.value.assign_from(value);
913        }
914        self.count += 1;
915        self.note_event(node, ParameterEventKind::New, name);
916        Ok(())
917    }
918
919    /// The declare-time half of issue 1150: the range must be well-formed
920    /// and the initial value must satisfy it. ONE spelling for both declare
921    /// paths.
922    ///
923    /// phase-417 W4.a — it returns rclrs's two distinct names rather than a
924    /// `bool`, because the remedies differ: `InvalidRange` means fix the
925    /// range, `InitialValueOutOfRange` means fix the default. The `bool`
926    /// spelling below is kept for the caller that only has a
927    /// `ParameterError` channel, so there is still one predicate.
928    pub(crate) fn declaration_consistency(
929        descriptor: Option<&ParameterDescriptor>,
930        value: &ParameterValue,
931    ) -> Result<(), DeclarationError> {
932        match descriptor {
933            Some(desc) if !desc.range.is_valid() => Err(DeclarationError::InvalidRange),
934            Some(desc) if !desc.validate_range(value) => {
935                Err(DeclarationError::InitialValueOutOfRange)
936            }
937            _ => Ok(()),
938        }
939    }
940
941    fn declaration_is_consistent(
942        descriptor: Option<&ParameterDescriptor>,
943        value: &ParameterValue,
944    ) -> bool {
945        Self::declaration_consistency(descriptor, value).is_ok()
946    }
947
948    /// Would `set(name, value)` succeed on an EXISTING entry? Read-only,
949    /// then type, then range — the one ordering every set path uses.
950    fn check_set_existing(
951        entry: &ParameterEntry,
952        value: &ParameterValue,
953    ) -> Result<(), ParameterError> {
954        if let Some(ref desc) = entry.descriptor {
955            if desc.read_only {
956                return Err(ParameterError::ReadOnly);
957            }
958            if !desc.dynamic_typing && desc.param_type != value.param_type() {
959                return Err(ParameterError::TypeMismatch);
960            }
961            if !desc.validate_range(value) {
962                return Err(ParameterError::OutOfRange);
963            }
964        }
965        Ok(())
966    }
967
968    /// Dry-run of [`apply`](Self::apply): the verdict a wire-facing set would
969    /// get, without changing anything. `SetParametersAtomically` runs this
970    /// over the whole batch before applying any of it.
971    pub(crate) fn check_apply(
972        &self,
973        node: NodeKey,
974        name: &str,
975        value: &ParameterValue,
976    ) -> Result<(), ParameterError> {
977        match self
978            .find_index(node, name)
979            .and_then(|idx| self.table.entries[idx].as_ref())
980        {
981            Some(entry) => Self::check_set_existing(entry, value),
982            None if !self.allows_undeclared(node) => Err(ParameterError::Undeclared),
983            None if self.is_full() => Err(ParameterError::StorageFull),
984            None if name.len() > MAX_PARAM_NAME_LEN => Err(ParameterError::StorageFull),
985            None => Ok(()),
986        }
987    }
988
989    /// THE wire-facing set — what one element of `SetParameters` /
990    /// `SetParametersAtomically` does (issue 1151).
991    ///
992    /// An existing parameter is set through [`set`](Self::set). An unknown
993    /// name is [`Undeclared`](ParameterError::Undeclared) unless
994    /// [`set_allow_undeclared`](Self::set_allow_undeclared) was switched on
995    /// FOR THAT NODE, in which case it is declared (or `StorageFull`). Every
996    /// service handler and every facade writer goes through here so the
997    /// by-value oracle, the streaming path, the atomic pre-check and the C
998    /// setters cannot disagree about what "undeclared" means.
999    pub(crate) fn apply(
1000        &mut self,
1001        node: NodeKey,
1002        name: &str,
1003        value: ParameterValue,
1004    ) -> Result<(), ParameterError> {
1005        self.apply_ref(node, name, &value)
1006    }
1007
1008    /// [`apply`](Self::apply), borrowing the value -- issue 2104, and THE
1009    /// apply the by-value spelling forwards to. The value is copied once, into
1010    /// its slot (`ParameterValue::assign_from`); moved in, a ~8 KiB value is a
1011    /// copy in every frame it passes, which is what put a C++ array set at
1012    /// 16.6 KiB of Cortex-M3 stack.
1013    pub(crate) fn apply_ref(
1014        &mut self,
1015        node: NodeKey,
1016        name: &str,
1017        value: &ParameterValue,
1018    ) -> Result<(), ParameterError> {
1019        if matches!(value, ParameterValue::NotSet) && self.delete_on_not_set.get(node) {
1020            return self.apply_delete(node, name);
1021        }
1022        self.check_apply(node, name, value)?;
1023        // phase-417 W4.a — the accept/reject hook, AFTER the store's own rules
1024        // and BEFORE the write, which is rclcpp's ordering: a callback never
1025        // sees a write the type, range or read-only rules already refused, and
1026        // a callback that refuses leaves the value untouched. `apply` is the
1027        // one seam every writer funnels through (the six services, the C and
1028        // C++ facades, `Executor::set_parameter`), so hooking it here is what
1029        // makes one registration reach all of them.
1030        if !self.run_on_set_callbacks(node, name, self.get(node, name), Some(value)) {
1031            return Err(ParameterError::Rejected);
1032        }
1033        if self.find_index(node, name).is_some() {
1034            self.set_ref(node, name, value)
1035        } else if self.declare_ref(node, name, value) {
1036            Ok(())
1037        } else {
1038            Err(ParameterError::StorageFull)
1039        }
1040    }
1041
1042    /// [`apply`](Self::apply), taking the value from a caller-owned STAGING
1043    /// slot instead of by value — RFC-0105 D10.2 / issue 1812.
1044    ///
1045    /// A `ParameterValue` is sized by its largest variant (~8.5 KiB at the
1046    /// default bounds), so `apply(value)` puts one on the caller's stack, and
1047    /// the wire handler that decoded it put another there before that. A
1048    /// remote set decodes into a slot carved from the executor backing and
1049    /// hands it here: the same rules, hooks and order as `apply`, and the
1050    /// write is a SWAP between the slot and the stored value, so no
1051    /// value-sized temporary exists on any path that sets an existing
1052    /// parameter. On return `staged` holds the previous value (or whatever it
1053    /// held, when the set was refused); its content is scratch.
1054    ///
1055    /// The one path that still builds an entry is the undeclared add
1056    /// (`allow_undeclared`), which places an EMPTY entry first and then swaps
1057    /// the value into it; see `declare_placeholder`.
1058    pub(crate) fn apply_staged(
1059        &mut self,
1060        node: NodeKey,
1061        name: &str,
1062        staged: &mut ParameterValue,
1063    ) -> Result<(), ParameterError> {
1064        if matches!(staged, ParameterValue::NotSet) && self.delete_on_not_set.get(node) {
1065            return self.apply_delete(node, name);
1066        }
1067        self.check_apply(node, name, staged)?;
1068        if !self.run_on_set_callbacks(node, name, self.get(node, name), Some(staged)) {
1069            return Err(ParameterError::Rejected);
1070        }
1071        let (idx, kind) = match self.find_index(node, name) {
1072            Some(idx) => (idx, ParameterEventKind::Changed),
1073            None => match self.declare_placeholder(node, name) {
1074                Some(idx) => (idx, ParameterEventKind::New),
1075                None => return Err(ParameterError::StorageFull),
1076            },
1077        };
1078        match self.table.entries[idx].as_mut() {
1079            Some(entry) => {
1080                core::mem::swap(&mut entry.param.value, staged);
1081                self.note_event(node, kind, name);
1082                Ok(())
1083            }
1084            None => Err(ParameterError::NotFound),
1085        }
1086    }
1087
1088    /// Place a `NotSet` entry named `name` on `node` and return its index —
1089    /// the undeclared-add half of [`apply_staged`](Self::apply_staged).
1090    ///
1091    /// Out of line on purpose: an unoptimised build materialises the entry it
1092    /// writes, and that frame belongs on the rare path only, never on the
1093    /// frame every remote set runs in.
1094    #[inline(never)]
1095    fn declare_placeholder(&mut self, node: NodeKey, name: &str) -> Option<usize> {
1096        let slot = self.find_empty_slot()?;
1097        let mut stored = String::new();
1098        stored.push_str(name).ok()?;
1099        self.table.entries[slot] = Some(ParameterEntry {
1100            node,
1101            param: Parameter {
1102                name: stored,
1103                value: ParameterValue::NotSet,
1104            },
1105            descriptor: None,
1106        });
1107        self.count += 1;
1108        Some(slot)
1109    }
1110
1111    /// The delete half of [`apply`](Self::apply) for a node that asked for
1112    /// [`set_delete_on_not_set`](Self::set_delete_on_not_set): read-only
1113    /// refuses, the hooks see `(old, None)`, then the parameter is removed.
1114    fn apply_delete(&mut self, node: NodeKey, name: &str) -> Result<(), ParameterError> {
1115        let Some(entry) = self
1116            .find_index(node, name)
1117            .and_then(|idx| self.table.entries[idx].as_ref())
1118        else {
1119            return Err(ParameterError::NotFound);
1120        };
1121        if entry.descriptor.as_ref().is_some_and(|d| d.read_only) {
1122            return Err(ParameterError::ReadOnly);
1123        }
1124        if !self.run_on_set_callbacks(node, name, self.get(node, name), None) {
1125            return Err(ParameterError::Rejected);
1126        }
1127        self.remove(node, name);
1128        Ok(())
1129    }
1130
1131    /// phase-491 W2 (RFC-0105 D6/D7) — rclc's LOCAL typed set,
1132    /// `rclc_parameter_set_{bool,int,double}`.
1133    ///
1134    /// rclc's owner of a parameter is not bound by its own read-only flag or
1135    /// range: read-only is "read-only to the OUTSIDE", and rclc never checks a
1136    /// range at all. So this is type, then the hooks with `(old, new)`, then
1137    /// the write — and nothing else. Remote writes keep going through
1138    /// [`apply`](Self::apply), which enforces both.
1139    ///
1140    /// The type check compares against the STORED value's type unless the
1141    /// descriptor allows dynamic typing, because rclc refuses a typed set of
1142    /// the wrong type (`RCLC_PARAMETER_TYPE_MISMATCH`) whether or not a
1143    /// descriptor was ever attached.
1144    pub(crate) fn set_local(
1145        &mut self,
1146        node: NodeKey,
1147        name: &str,
1148        value: ParameterValue,
1149    ) -> Result<(), ParameterError> {
1150        let Some(idx) = self.find_index(node, name) else {
1151            return Err(ParameterError::NotFound);
1152        };
1153        let Some(entry) = self.table.entries[idx].as_ref() else {
1154            return Err(ParameterError::NotFound);
1155        };
1156        let dynamic = entry.descriptor.as_ref().is_some_and(|d| d.dynamic_typing);
1157        if !dynamic && entry.param.value.param_type() != value.param_type() {
1158            return Err(ParameterError::TypeMismatch);
1159        }
1160        if !self.run_on_set_callbacks(node, name, Some(&entry.param.value), Some(&value)) {
1161            return Err(ParameterError::Rejected);
1162        }
1163        if let Some(entry) = self.table.entries[idx].as_mut() {
1164            entry.param.value = value;
1165        }
1166        self.note_event(node, ParameterEventKind::Changed, name);
1167        Ok(())
1168    }
1169
1170    /// Run every registered hook for `node`. `false` from any one of them
1171    /// refuses the write; the rest are not consulted, as rclcpp stops at the
1172    /// first unsuccessful result.
1173    fn run_on_set_callbacks(
1174        &self,
1175        node: NodeKey,
1176        name: &str,
1177        old: Option<&ParameterValue>,
1178        new: Option<&ParameterValue>,
1179    ) -> bool {
1180        for slot in self.on_set.iter().flatten() {
1181            if slot.node == node && !(slot.callback)(node, name, old, new, slot.ctx) {
1182                return false;
1183            }
1184        }
1185        true
1186    }
1187
1188    /// Register an accept/reject hook for writes to `node` — rclcpp's
1189    /// `add_on_set_parameters_callback` (phase-417 W4.a).
1190    ///
1191    /// The callback runs on every [`apply`](Self::apply): a remote
1192    /// `ros2 param set`, a C++
1193    /// `Node::set_parameter`, a Rust `Executor::set_parameter`. Returning
1194    /// `false` refuses the write with [`ParameterError::Rejected`] and
1195    /// leaves the stored value alone.
1196    ///
1197    /// `None` when all [`MAX_ON_SET_CALLBACKS`] slots are taken — a refusal,
1198    /// never a silent eviction.
1199    pub(crate) fn add_on_set_parameters_callback(
1200        &mut self,
1201        node: NodeKey,
1202        callback: OnSetParameterFn,
1203        ctx: OnSetContext,
1204    ) -> Option<OnSetParameterHandle> {
1205        let idx = self.on_set.iter().position(Option::is_none)?;
1206        self.on_set[idx] = Some(OnSetCallbackSlot {
1207            node,
1208            callback,
1209            ctx,
1210        });
1211        Some(OnSetParameterHandle(idx as u16))
1212    }
1213
1214    /// Unregister a hook — rclcpp's `remove_on_set_parameters_callback`.
1215    /// `false` if the slot was already empty.
1216    pub(crate) fn remove_on_set_parameters_callback(
1217        &mut self,
1218        handle: OnSetParameterHandle,
1219    ) -> bool {
1220        let idx = handle.0 as usize;
1221        if idx >= MAX_ON_SET_CALLBACKS || self.on_set[idx].is_none() {
1222            return false;
1223        }
1224        self.on_set[idx] = None;
1225        true
1226    }
1227
1228    /// How many hook slots are free. Lets a caller ask before registering
1229    /// rather than discovering the bound as a `None`.
1230    pub(crate) fn on_set_callbacks_free(&self) -> usize {
1231        self.on_set.iter().filter(|s| s.is_none()).count()
1232    }
1233
1234    // ---- descriptor mutation on a DECLARED parameter (phase-417 W4.a) ------
1235    //
1236    // rclc's shape: `rclc_add_parameter_description`,
1237    // `rclc_add_parameter_constraint_double` / `_integer` and
1238    // `rclc_set_parameter_read_only` all attach metadata AFTER the declaration,
1239    // so a C caller that cannot build a descriptor value still gets one. Ours
1240    // are the same verbs against the one store, and they are what the C and C++
1241    // descriptor surfaces forward to — a wrapper that built its own descriptor
1242    // table would be the second store phase-426 removed.
1243    //
1244    // Each creates the descriptor on demand: a parameter declared without one
1245    // has `None` in its slot, and refusing to describe it would make "declare
1246    // then describe" work only in the order rclc does not use.
1247
1248    /// The descriptor of `node`'s `name`, creating an empty one typed from the
1249    /// stored value if the declaration carried none.
1250    fn descriptor_mut(&mut self, node: NodeKey, name: &str) -> Option<&mut ParameterDescriptor> {
1251        let idx = self.find_index(node, name)?;
1252        let entry = self.table.entries[idx].as_mut()?;
1253        if entry.descriptor.is_none() {
1254            entry.descriptor = Some(ParameterDescriptor::new(
1255                name,
1256                entry.param.value.param_type(),
1257            )?);
1258        }
1259        entry.descriptor.as_mut()
1260    }
1261
1262    /// Attach a description and free-text constraints to a declared parameter
1263    /// — rclc's `rclc_add_parameter_description`. Either text may be empty.
1264    /// `false` if the parameter is not declared on `node`.
1265    pub(crate) fn set_parameter_description(
1266        &mut self,
1267        node: NodeKey,
1268        name: &str,
1269        description: &str,
1270        additional_constraints: &str,
1271    ) -> bool {
1272        let mut truncated = false;
1273        let found = match self.descriptor_mut(node, name) {
1274            Some(desc) => {
1275                truncated |= desc.set_description(description);
1276                truncated |= desc.set_additional_constraints(additional_constraints);
1277                true
1278            }
1279            None => false,
1280        };
1281        // Same report path a declared-with-descriptor truncation takes, so the
1282        // executor logs it once whichever verb supplied the text.
1283        self.truncation_pending |= truncated;
1284        found
1285    }
1286
1287    /// Mark a declared parameter read-only, so every later write is
1288    /// [`ParameterError::ReadOnly`] — rclc's
1289    /// `rclc_set_parameter_read_only`. `false` if not declared on `node`.
1290    pub(crate) fn set_parameter_read_only(
1291        &mut self,
1292        node: NodeKey,
1293        name: &str,
1294        read_only: bool,
1295    ) -> bool {
1296        match self.descriptor_mut(node, name) {
1297            Some(desc) => {
1298                desc.read_only = read_only;
1299                true
1300            }
1301            None => false,
1302        }
1303    }
1304
1305    /// Attach an integer range to a declared parameter — rclc's
1306    /// `rclc_add_parameter_constraint_integer`.
1307    ///
1308    /// Refuses an ill-formed range, and refuses one the CURRENT value does not
1309    /// satisfy: attaching a constraint that the stored value already violates
1310    /// would leave the store in a state no `set` could have produced, and
1311    /// `ros2 param describe` would then advertise a bound the value breaks.
1312    pub(crate) fn set_parameter_integer_range(
1313        &mut self,
1314        node: NodeKey,
1315        name: &str,
1316        min: i64,
1317        max: i64,
1318        step: i64,
1319    ) -> bool {
1320        self.set_parameter_range(
1321            node,
1322            name,
1323            ParameterRange::Integer(IntegerRange::new(min, max, step)),
1324        )
1325    }
1326
1327    /// Attach a floating-point range to a declared parameter — rclc's
1328    /// `rclc_add_parameter_constraint_double`. See
1329    /// [`set_parameter_integer_range`](Self::set_parameter_integer_range).
1330    pub(crate) fn set_parameter_float_range(
1331        &mut self,
1332        node: NodeKey,
1333        name: &str,
1334        min: f64,
1335        max: f64,
1336        step: f64,
1337    ) -> bool {
1338        self.set_parameter_range(
1339            node,
1340            name,
1341            ParameterRange::FloatingPoint(FloatingPointRange::new(min, max, step)),
1342        )
1343    }
1344
1345    /// The one range-attaching path both spellings above use.
1346    fn set_parameter_range(&mut self, node: NodeKey, name: &str, range: ParameterRange) -> bool {
1347        if !range.is_valid() {
1348            return false;
1349        }
1350        let idx = match self.find_index(node, name) {
1351            Some(idx) => idx,
1352            None => return false,
1353        };
1354        // The stored value has to satisfy the range BEFORE it is attached, and
1355        // reading it needs the entry, so the check happens here rather than
1356        // inside `descriptor_mut`. Borrowed, not cloned (issue 2104): the clone
1357        // was a 16.9 KiB frame on Cortex-M3 for a range check.
1358        match self.table.entries[idx].as_ref() {
1359            Some(entry) if range.contains(&entry.param.value) => {}
1360            _ => return false,
1361        }
1362        match self.descriptor_mut(node, name) {
1363            Some(desc) => {
1364                desc.range = range;
1365                true
1366            }
1367            None => false,
1368        }
1369    }
1370
1371    /// All-or-nothing multi-set — `~/set_parameters_atomically`'s local twin
1372    /// (phase-417 W4.a).
1373    ///
1374    /// We SERVED this and offered it in no language, so the only way to reach
1375    /// our own atomic set was over the wire. This is the same two passes the
1376    /// service handler makes: every item is checked against the CURRENT state,
1377    /// and only if all pass is any applied.
1378    ///
1379    /// `items` yields `None` for an element that could not even be turned into
1380    /// a value — which is how the service handler reports a wire value that
1381    /// does not fit the store's capacity (issue 0323) — and a `None` refuses
1382    /// the batch like any other failing check.
1383    ///
1384    /// It is `Clone` because the two passes walk it twice; a slice iterator is.
1385    ///
1386    /// Returns `Ok(())`, or the FIRST verdict that
1387    /// refused. Nothing is written on a refusal.
1388    ///
1389    /// The per-item check is against the state BEFORE the batch, not against
1390    /// the state each earlier item would leave. That is the service handler's
1391    /// contract verbatim, and it matters in one case: a batch that declares
1392    /// more undeclared names than there are free slots passes the check and
1393    /// fills what it can. Making the pre-check cumulative would need a
1394    /// speculative copy of the table, which is the allocation this store does
1395    /// not have.
1396    pub(crate) fn apply_atomically<'x, I>(
1397        &mut self,
1398        node: NodeKey,
1399        items: I,
1400    ) -> Result<(), ParameterError>
1401    where
1402        I: Iterator<Item = Option<(&'x str, ParameterValue)>> + Clone,
1403    {
1404        // Issue 2104 -- ONE implementation of the two passes, in
1405        // `apply_atomically_with`; this adapts an iterator to its fill
1406        // callback, restarting the iterator at each pass's first element.
1407        let count = items.clone().count();
1408        let mut it = items.clone();
1409        self.apply_atomically_with(node, count, |i, slot| {
1410            if i == 0 {
1411                it = items.clone();
1412            }
1413            let (name, value) = it.next()??;
1414            slot.assign_from(&value);
1415            Some(name)
1416        })
1417    }
1418
1419    /// [`apply_atomically`](Self::apply_atomically), with element `i` FILLED
1420    /// into one scratch value by `fill(i, slot)` -- issue 2104.
1421    ///
1422    /// `fill` returns the element's name, or `None` for an element that could
1423    /// not be turned into a value (which refuses the batch, as a `None` item
1424    /// does). It is called for `0..count` in order, once per pass, so it must
1425    /// fill the same value for the same `i` both times.
1426    ///
1427    /// The iterator form hands each ~8 KiB value out by value, so a C++
1428    /// `set_parameters_atomically` held one in its closure's frame and more in
1429    /// its own (25 KiB on Cortex-M3). Here the only value is `slot`, which a
1430    /// caller fills in place (`ParameterValue::assign_string`, …).
1431    pub(crate) fn apply_atomically_with<'x>(
1432        &mut self,
1433        node: NodeKey,
1434        count: usize,
1435        mut fill: impl FnMut(usize, &mut ParameterValue) -> Option<&'x str>,
1436    ) -> Result<(), ParameterError> {
1437        let mut slot = ParameterValue::NotSet;
1438        for i in 0..count {
1439            let Some(name) = fill(i, &mut slot) else {
1440                return Err(ParameterError::TypeMismatch);
1441            };
1442            self.check_apply(node, name, &slot)?;
1443            // The hook is consulted in the CHECK pass, so a callback that
1444            // refuses one element refuses the batch before anything is
1445            // written. `apply` will consult it again on the write pass; a hook
1446            // that answers differently to the same value twice is the caller's
1447            // own inconsistency, and the second answer is the one that stands.
1448            if !self.run_on_set_callbacks(node, name, self.get(node, name), Some(&slot)) {
1449                return Err(ParameterError::Rejected);
1450            }
1451        }
1452        for i in 0..count {
1453            let Some(name) = fill(i, &mut slot) else {
1454                continue;
1455            };
1456            let _ = self.apply_ref(node, name, &slot);
1457        }
1458        Ok(())
1459    }
1460
1461    /// Get a parameter value by name
1462    pub(crate) fn get(&self, node: NodeKey, name: &str) -> Option<&ParameterValue> {
1463        self.find_index(node, name)
1464            .and_then(|idx| self.table.entries[idx].as_ref())
1465            .map(|entry| &entry.param.value)
1466    }
1467
1468    /// Get a parameter by name
1469    pub(crate) fn get_parameter(&self, node: NodeKey, name: &str) -> Option<&Parameter> {
1470        self.find_index(node, name)
1471            .and_then(|idx| self.table.entries[idx].as_ref())
1472            .map(|entry| &entry.param)
1473    }
1474
1475    /// Get a parameter descriptor by name
1476    pub(crate) fn get_descriptor(&self, node: NodeKey, name: &str) -> Option<&ParameterDescriptor> {
1477        self.find_index(node, name)
1478            .and_then(|idx| self.table.entries[idx].as_ref())
1479            .and_then(|entry| entry.descriptor.as_ref())
1480    }
1481
1482    /// Set a parameter value
1483    ///
1484    /// Returns the result of the set operation.
1485    pub(crate) fn set(
1486        &mut self,
1487        node: NodeKey,
1488        name: &str,
1489        value: ParameterValue,
1490    ) -> Result<(), ParameterError> {
1491        self.set_ref(node, name, &value)
1492    }
1493
1494    /// [`set`](Self::set), borrowing the value (issue 2104): it is copied
1495    /// once, into the stored value.
1496    pub(crate) fn set_ref(
1497        &mut self,
1498        node: NodeKey,
1499        name: &str,
1500        value: &ParameterValue,
1501    ) -> Result<(), ParameterError> {
1502        let idx = match self.find_index(node, name) {
1503            Some(idx) => idx,
1504            None => return Err(ParameterError::NotFound),
1505        };
1506
1507        let entry = match self.table.entries[idx].as_mut() {
1508            Some(e) => e,
1509            None => return Err(ParameterError::NotFound),
1510        };
1511
1512        // Read-only, type, range (min/max/step) — one predicate, shared with
1513        // the atomic pre-check.
1514        Self::check_set_existing(entry, value)?;
1515
1516        entry.param.value.assign_from(value);
1517        self.note_event(node, ParameterEventKind::Changed, name);
1518        Ok(())
1519    }
1520
1521    /// Unset an optional parameter (set to NotSet)
1522    ///
1523    /// This bypasses the type check to allow optional parameters to be unset.
1524    /// Returns an error if the parameter is read-only.
1525    pub(crate) fn unset(&mut self, node: NodeKey, name: &str) -> Result<(), ParameterError> {
1526        let idx = match self.find_index(node, name) {
1527            Some(idx) => idx,
1528            None => return Err(ParameterError::NotFound),
1529        };
1530
1531        let entry = match self.table.entries[idx].as_mut() {
1532            Some(e) => e,
1533            None => return Err(ParameterError::NotFound),
1534        };
1535
1536        // Check if read-only
1537        if let Some(ref desc) = entry.descriptor
1538            && desc.read_only
1539        {
1540            return Err(ParameterError::ReadOnly);
1541        }
1542
1543        entry.param.value = ParameterValue::NotSet;
1544        self.note_event(node, ParameterEventKind::Changed, name);
1545        Ok(())
1546    }
1547
1548    /// Set or declare a parameter — an EXPLICIT auto-declare for in-image
1549    /// callers.
1550    ///
1551    /// If the parameter exists, sets its value. Otherwise, declares it. This
1552    /// ignores `allow_undeclared` on purpose: the flag governs what a REMOTE
1553    /// set may do; a Rust caller naming this method has opted in by naming
1554    /// it. Nothing on the wire path — and, since phase-426 W2, nothing on the
1555    /// C or C++ facade path either — reaches here (issue 1151).
1556    pub(crate) fn set_or_declare(
1557        &mut self,
1558        node: NodeKey,
1559        name: &str,
1560        value: ParameterValue,
1561    ) -> Result<(), ParameterError> {
1562        if self.find_index(node, name).is_some() {
1563            self.set(node, name, value)
1564        } else if self.declare(node, name, value) {
1565            Ok(())
1566        } else {
1567            Err(ParameterError::StorageFull)
1568        }
1569    }
1570
1571    /// Check if a parameter exists
1572    pub(crate) fn has(&self, node: NodeKey, name: &str) -> bool {
1573        self.find_index(node, name).is_some()
1574    }
1575
1576    /// Remove a parameter
1577    ///
1578    /// Returns true if the parameter was removed.
1579    pub(crate) fn remove(&mut self, node: NodeKey, name: &str) -> bool {
1580        if let Some(idx) = self.find_index(node, name) {
1581            self.table.entries[idx] = None;
1582            self.count -= 1;
1583            self.note_event(node, ParameterEventKind::Deleted, name);
1584            true
1585        } else {
1586            false
1587        }
1588    }
1589
1590    /// Get the type of a parameter
1591    pub(crate) fn get_type(&self, node: NodeKey, name: &str) -> Option<ParameterType> {
1592        self.get(node, name).map(|v| v.param_type())
1593    }
1594
1595    /// Iterate over `node`'s parameters
1596    pub(crate) fn iter(&self, node: NodeKey) -> impl Iterator<Item = &Parameter> {
1597        self.entries_of(node).map(|e| &e.param)
1598    }
1599
1600    /// List `node`'s parameter names
1601    pub(crate) fn list_names(&self, node: NodeKey) -> impl Iterator<Item = &str> {
1602        self.iter(node).map(|p| p.name.as_str())
1603    }
1604
1605    /// List `node`'s parameter names with a given prefix
1606    pub(crate) fn list_with_prefix<'a>(
1607        &'a self,
1608        node: NodeKey,
1609        prefix: &'a str,
1610    ) -> impl Iterator<Item = &'a str> {
1611        self.list_names(node)
1612            .filter(move |name| name.starts_with(prefix))
1613    }
1614
1615    /// Get a bool parameter value
1616    pub(crate) fn get_bool(&self, node: NodeKey, name: &str) -> Option<bool> {
1617        self.get(node, name).and_then(|v| v.as_bool())
1618    }
1619
1620    /// Get an integer parameter value
1621    pub(crate) fn get_integer(&self, node: NodeKey, name: &str) -> Option<i64> {
1622        self.get(node, name).and_then(|v| v.as_integer())
1623    }
1624
1625    /// Get a double parameter value
1626    pub(crate) fn get_double(&self, node: NodeKey, name: &str) -> Option<f64> {
1627        self.get(node, name).and_then(|v| v.as_double())
1628    }
1629
1630    /// Get a string parameter value
1631    pub(crate) fn get_string(&self, node: NodeKey, name: &str) -> Option<&str> {
1632        self.get(node, name).and_then(|v| v.as_string())
1633    }
1634
1635    /// Set a bool parameter value
1636    pub(crate) fn set_bool(
1637        &mut self,
1638        node: NodeKey,
1639        name: &str,
1640        value: bool,
1641    ) -> Result<(), ParameterError> {
1642        self.set(node, name, ParameterValue::Bool(value))
1643    }
1644
1645    /// Set an integer parameter value
1646    pub(crate) fn set_integer(
1647        &mut self,
1648        node: NodeKey,
1649        name: &str,
1650        value: i64,
1651    ) -> Result<(), ParameterError> {
1652        self.set(node, name, ParameterValue::Integer(value))
1653    }
1654
1655    /// Set a double parameter value
1656    pub(crate) fn set_double(
1657        &mut self,
1658        node: NodeKey,
1659        name: &str,
1660        value: f64,
1661    ) -> Result<(), ParameterError> {
1662        self.set(node, name, ParameterValue::Double(value))
1663    }
1664
1665    /// Set a string parameter value
1666    pub(crate) fn set_string(
1667        &mut self,
1668        node: NodeKey,
1669        name: &str,
1670        value: &str,
1671    ) -> Result<(), ParameterError> {
1672        match ParameterValue::from_string(value) {
1673            Some(v) => self.set(node, name, v),
1674            None => Err(ParameterError::StorageFull), // String too long
1675        }
1676    }
1677
1678    /// Declare a parameter with a descriptor and optional initial value.
1679    ///
1680    /// This is a more comprehensive declaration method used by the typed parameter API.
1681    ///
1682    /// # Arguments
1683    /// - `node`: The node the parameter belongs to.
1684    /// - `descriptor`: The metadata for the parameter.
1685    /// - `initial_value`: An optional initial value for the parameter. If `None`,
1686    ///   the parameter will be initialized to `ParameterValue::NotSet`.
1687    pub(crate) fn declare_parameter(
1688        &mut self,
1689        node: NodeKey,
1690        descriptor: ParameterDescriptor,
1691        initial_value: Option<&ParameterValue>,
1692    ) -> Result<(), ParameterError> {
1693        let name = descriptor.name.as_str();
1694
1695        // Check if already exists
1696        if self.find_index(node, name).is_some() {
1697            return Err(ParameterError::TypeMismatch); // Indicates already declared
1698        }
1699
1700        let param_value = initial_value.cloned().unwrap_or_default();
1701
1702        // Issue 1150 — same declare-time check as `declare_with_descriptor`.
1703        if !Self::declaration_is_consistent(Some(&descriptor), &param_value) {
1704            return Err(ParameterError::InvalidRange);
1705        }
1706
1707        // Find an empty slot
1708        let slot = match self.find_empty_slot() {
1709            Some(idx) => idx,
1710            None => return Err(ParameterError::StorageFull),
1711        };
1712
1713        let param = Parameter::new(name, param_value).ok_or(ParameterError::StorageFull)?; // name too long
1714
1715        self.note_descriptor(Some(&descriptor));
1716        self.table.entries[slot] = Some(ParameterEntry {
1717            node,
1718            param,
1719            descriptor: Some(descriptor),
1720        });
1721        self.count += 1;
1722        Ok(())
1723    }
1724
1725    /// Get the current value of a parameter.
1726    ///
1727    /// Returns `Some(ParameterValue)` if the parameter exists, `None` otherwise.
1728    /// The `ParameterValue` is cloned to avoid lifetime issues with `&mut self`.
1729    pub(crate) fn get_parameter_value(&self, node: NodeKey, name: &str) -> Option<ParameterValue> {
1730        self.get(node, name).cloned()
1731    }
1732
1733    /// Set the value of a parameter.
1734    ///
1735    /// Returns `Ok(())` on success, or an error if the parameter
1736    /// is read-only, type mismatches, or is out of range.
1737    pub(crate) fn set_parameter_value(
1738        &mut self,
1739        node: NodeKey,
1740        name: &str,
1741        value: ParameterValue,
1742    ) -> Result<(), ParameterError> {
1743        self.set(node, name, value)
1744    }
1745}
1746
1747/// Builder for declaring parameters with a fluent API
1748pub struct LegacyParameterBuilder<'a, 's> {
1749    server: &'a mut ParameterServer<'s>,
1750    /// phase-426 W1 — the node this declaration belongs to, fixed when the
1751    /// builder is made so no chained call can lose it.
1752    node: NodeKey,
1753    name: String<MAX_PARAM_NAME_LEN>,
1754    value: ParameterValue,
1755    descriptor: Option<ParameterDescriptor>,
1756}
1757
1758impl<'a, 's> LegacyParameterBuilder<'a, 's> {
1759    /// Create a new parameter builder
1760    pub fn new(
1761        server: &'a mut ParameterServer<'s>,
1762        node: NodeKey,
1763        name: &str,
1764        value: ParameterValue,
1765    ) -> Option<Self> {
1766        let mut n = String::new();
1767        n.push_str(name).ok()?;
1768        Some(Self {
1769            server,
1770            node,
1771            name: n,
1772            value,
1773            descriptor: None,
1774        })
1775    }
1776
1777    /// Set the parameter description
1778    pub fn description(mut self, desc: &str) -> Self {
1779        let param_type = self.value.param_type();
1780        if self.descriptor.is_none() {
1781            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
1782        }
1783        // phase-446 F2 -- truncated at a character boundary and recorded,
1784        // where `let _ = push_str` stored nothing for a too-long description.
1785        if let Some(ref mut d) = self.descriptor {
1786            d.set_description(desc);
1787        }
1788        self
1789    }
1790
1791    /// Set the parameter as read-only
1792    pub fn read_only(mut self) -> Self {
1793        let param_type = self.value.param_type();
1794        if self.descriptor.is_none() {
1795            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
1796        }
1797        if let Some(ref mut d) = self.descriptor {
1798            d.read_only = true;
1799        }
1800        self
1801    }
1802
1803    /// Set integer range constraints
1804    pub fn integer_range(mut self, min: i64, max: i64, step: i64) -> Self {
1805        let param_type = self.value.param_type();
1806        if self.descriptor.is_none() {
1807            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
1808        }
1809        if let Some(ref mut d) = self.descriptor {
1810            d.range = crate::types::ParameterRange::Integer(crate::types::IntegerRange::new(
1811                min, max, step,
1812            ));
1813        }
1814        self
1815    }
1816
1817    /// Set float range constraints
1818    pub fn float_range(mut self, min: f64, max: f64, step: f64) -> Self {
1819        let param_type = self.value.param_type();
1820        if self.descriptor.is_none() {
1821            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
1822        }
1823        if let Some(ref mut d) = self.descriptor {
1824            d.range = crate::types::ParameterRange::FloatingPoint(
1825                crate::types::FloatingPointRange::new(min, max, step),
1826            );
1827        }
1828        self
1829    }
1830
1831    /// Declare the parameter
1832    ///
1833    /// phase-428 W6 remainder: the terminal of a builder chain, so a dropped
1834    /// answer is a parameter that was configured in full and then not declared.
1835    #[must_use]
1836    pub fn declare(self) -> bool {
1837        self.server.declare_with_descriptor(
1838            self.node,
1839            self.name.as_str(),
1840            self.value,
1841            self.descriptor,
1842        )
1843    }
1844}
1845
1846#[cfg(test)]
1847mod tests {
1848    use super::*;
1849
1850    /// phase-426 W1 — the node these single-node tests are about.
1851    const NODE: NodeKey = NodeKey::PRIMARY;
1852    use crate::types::{ParameterDescriptor, ParameterType};
1853
1854    // ── issue 2070: a borrowed declaration stores exactly what it was lent ──
1855
1856    fn every_variant() -> std::vec::Vec<ParameterValue> {
1857        let mut strings = ParameterValue::StringArray(Default::default());
1858        if let ParameterValue::StringArray(v) = &mut strings {
1859            for e in ["a", "", "frame_id"] {
1860                let mut s = heapless::String::new();
1861                s.push_str(e).unwrap();
1862                v.push(s).unwrap();
1863            }
1864        }
1865        std::vec![
1866            ParameterValue::Bool(true),
1867            ParameterValue::Integer(-7),
1868            ParameterValue::Double(2.5),
1869            ParameterValue::from_string("base_link").unwrap(),
1870            ParameterValue::from_byte_array(&[1, 2, 3]).unwrap(),
1871            ParameterValue::from_bool_array(&[true, false]).unwrap(),
1872            ParameterValue::from_integer_array(&[4, 5, 6]).unwrap(),
1873            ParameterValue::from_double_array(&[0.5, -1.0]).unwrap(),
1874            strings,
1875        ]
1876    }
1877
1878    /// `declare_ref` copies the value into its slot once, and what the slot
1879    /// then holds is the value it was lent, for every variant.
1880    #[test]
1881    fn a_borrowed_declaration_stores_the_value_it_was_lent() {
1882        let values = every_variant();
1883        let mut storage = ParameterStorage::<16>::new();
1884        let mut server = ParameterServer::new_in(storage.as_table());
1885        for (i, v) in values.iter().enumerate() {
1886            let name = std::format!("p{i}");
1887            assert!(server.declare_ref(NODE, &name, v));
1888            assert_eq!(
1889                std::format!("{:?}", server.get(NODE, &name).unwrap()),
1890                std::format!("{v:?}")
1891            );
1892        }
1893        assert_eq!(server.len(NODE), values.len());
1894        // A second declaration of a name is refused and leaves the first.
1895        assert!(!server.declare_ref(NODE, "p0", &ParameterValue::Integer(1)));
1896        assert!(matches!(
1897            server.get(NODE, "p0"),
1898            Some(ParameterValue::Bool(true))
1899        ));
1900    }
1901
1902    /// `assign_from` replaces whatever the destination held, including a
1903    /// different variant and a longer value of the same one.
1904    #[test]
1905    fn assign_from_replaces_any_previous_value() {
1906        let values = every_variant();
1907        for dst_init in &values {
1908            for src in &values {
1909                let mut dst = dst_init.clone();
1910                dst.assign_from(src);
1911                assert_eq!(std::format!("{dst:?}"), std::format!("{src:?}"));
1912            }
1913        }
1914        let mut long = ParameterValue::from_integer_array(&[1, 2, 3, 4]).unwrap();
1915        long.assign_from(&ParameterValue::from_integer_array(&[9]).unwrap());
1916        assert_eq!(
1917            std::format!("{long:?}"),
1918            std::format!("{:?}", ParameterValue::from_integer_array(&[9]).unwrap())
1919        );
1920    }
1921
1922    // ── phase-446 F2: a truncated description is reported once ──
1923
1924    /// Every declare path records a truncated description on the stored
1925    /// descriptor -- the plain descriptor, the typed builder and the legacy
1926    /// builder -- and `take_truncated_descriptions` hands each one over
1927    /// exactly once, with its node and name. A description that fits is
1928    /// never reported.
1929    #[test]
1930    fn each_truncated_description_is_handed_over_once() {
1931        let long = "y".repeat(crate::MAX_PARAM_DESCRIPTION_LEN + 1);
1932        let other = NodeKey::new(1);
1933        let mut storage = ParameterStorage::<8>::new();
1934        let mut server = ParameterServer::new_in(storage.as_table());
1935        assert_eq!(server.take_truncated_descriptions(|_, _| ()), 0);
1936
1937        let d = ParameterDescriptor::new("plain", ParameterType::Integer)
1938            .unwrap()
1939            .with_description(&long);
1940        assert!(server.declare_with_descriptor(NODE, "plain", ParameterValue::Integer(1), Some(d)));
1941        let fits = ParameterDescriptor::new("short", ParameterType::Integer)
1942            .unwrap()
1943            .with_description("");
1944        assert!(server.declare_with_descriptor(
1945            NODE,
1946            "short",
1947            ParameterValue::Integer(1),
1948            Some(fits)
1949        ));
1950        crate::ParameterBuilder::<i64>::new(&mut server, other, "typed")
1951            .default(3)
1952            .description(&long)
1953            .optional()
1954            .expect("a long description no longer refuses the declaration");
1955        assert!(
1956            LegacyParameterBuilder::new(&mut server, NODE, "legacy", ParameterValue::Bool(true))
1957                .unwrap()
1958                .description(&long)
1959                .declare()
1960        );
1961
1962        let mut seen: heapless::Vec<(usize, heapless::String<16>), 8> = heapless::Vec::new();
1963        let n = server.take_truncated_descriptions(|node, name| {
1964            let mut s = heapless::String::new();
1965            s.push_str(name).unwrap();
1966            seen.push((node.index(), s)).unwrap();
1967        });
1968        assert_eq!(n, 3, "{seen:?}");
1969        for (node, name) in [(0, "plain"), (1, "typed"), (0, "legacy")] {
1970            assert!(
1971                seen.iter().any(|(k, s)| *k == node && s.as_str() == name),
1972                "{name} on node {node} not reported: {seen:?}"
1973            );
1974        }
1975        assert!(!seen.iter().any(|(_, s)| s.as_str() == "short"));
1976        assert_eq!(
1977            server.take_truncated_descriptions(|_, _| ()),
1978            0,
1979            "once per parameter"
1980        );
1981        let kept = server.get_descriptor(other, "typed").unwrap();
1982        assert_eq!(kept.description.len(), crate::MAX_PARAM_DESCRIPTION_LEN);
1983    }
1984
1985    // ── phase-426 W1: the store is keyed by NODE ──
1986
1987    /// Two nodes composed onto one executor (RFC-0047) declaring the same
1988    /// name. Before W1 the table was flat, so the second `declare` returned
1989    /// `false` (already exists) and both nodes read ONE value: `/talker`'s
1990    /// `rate` and `/listener`'s `rate` were the same parameter.
1991    #[test]
1992    fn two_nodes_hold_independent_values_for_one_name() {
1993        let talker = NodeKey::new(0);
1994        let listener = NodeKey::new(1);
1995        let mut storage: ParameterStorage = ParameterStorage::new();
1996        let mut server = ParameterServer::new_in(storage.as_table());
1997
1998        assert!(server.declare(talker, "rate", ParameterValue::Integer(10)));
1999        // The pre-W1 collision: a flat table refuses this as a duplicate.
2000        assert!(
2001            server.declare(listener, "rate", ParameterValue::Integer(20)),
2002            "a sibling node's identical name is a DIFFERENT parameter"
2003        );
2004
2005        assert_eq!(server.get_integer(talker, "rate"), Some(10));
2006        assert_eq!(server.get_integer(listener, "rate"), Some(20));
2007
2008        // A set on one is invisible to the other.
2009        assert_eq!(
2010            server.set(talker, "rate", ParameterValue::Integer(11)),
2011            Ok(())
2012        );
2013        assert_eq!(server.get_integer(talker, "rate"), Some(11));
2014        assert_eq!(server.get_integer(listener, "rate"), Some(20));
2015
2016        // And each node enumerates only its own.
2017        assert_eq!(server.len(talker), 1);
2018        assert_eq!(server.len(listener), 1);
2019        assert_eq!(server.total_len(), 2, "one shared arena, two entries");
2020    }
2021
2022    /// A name one node declared is not a name its sibling has.
2023    #[test]
2024    fn a_sibling_cannot_read_or_write_a_name_it_never_declared() {
2025        let a = NodeKey::new(0);
2026        let b = NodeKey::new(3);
2027        let mut storage: ParameterStorage = ParameterStorage::new();
2028        let mut server = ParameterServer::new_in(storage.as_table());
2029        assert!(server.declare(a, "gain", ParameterValue::Double(1.0)));
2030
2031        assert!(!server.has(b, "gain"));
2032        assert!(server.get(b, "gain").is_none());
2033        assert_eq!(
2034            server.set(b, "gain", ParameterValue::Double(2.0)),
2035            Err(ParameterError::NotFound)
2036        );
2037        assert_eq!(
2038            server.check_apply(b, "gain", &ParameterValue::Double(2.0)),
2039            Err(ParameterError::Undeclared)
2040        );
2041        // Untouched.
2042        assert_eq!(server.get_double(a, "gain"), Some(1.0));
2043    }
2044
2045    /// `allow_undeclared` is a NODE option upstream, so one node opting in
2046    /// must not decide what a remote set may do to its sibling (issue 1151 +
2047    /// phase-426 W1).
2048    #[test]
2049    fn allow_undeclared_does_not_leak_to_a_sibling_node() {
2050        let permissive = NodeKey::new(1);
2051        let strict = NodeKey::new(2);
2052        let mut storage: ParameterStorage = ParameterStorage::new();
2053        let mut server = ParameterServer::new_in(storage.as_table());
2054        server.set_allow_undeclared(permissive, true);
2055
2056        assert!(server.allows_undeclared(permissive));
2057        assert!(!server.allows_undeclared(strict));
2058
2059        assert_eq!(
2060            server.apply(permissive, "fresh", ParameterValue::Integer(7)),
2061            Ok(())
2062        );
2063        assert_eq!(
2064            server.apply(strict, "fresh", ParameterValue::Integer(7)),
2065            Err(ParameterError::Undeclared),
2066            "the sibling still refuses a name IT never declared"
2067        );
2068        assert_eq!(server.get_integer(permissive, "fresh"), Some(7));
2069        assert!(!server.has(strict, "fresh"));
2070    }
2071
2072    /// The flag reaches every key in a `u8`'s range, not just the low ones —
2073    /// the bitmask is `[u64; 4]` precisely so there is no ceiling to find at
2074    /// runtime.
2075    #[test]
2076    fn the_allow_undeclared_flag_covers_the_whole_key_space() {
2077        let mut storage: ParameterStorage = ParameterStorage::new();
2078        let mut server = ParameterServer::new_in(storage.as_table());
2079        for raw in [0u8, 63, 64, 127, 128, 191, 192, 255] {
2080            let node = NodeKey::new(raw);
2081            assert!(!server.allows_undeclared(node), "raw {raw}");
2082            server.set_allow_undeclared(node, true);
2083            assert!(server.allows_undeclared(node), "raw {raw}");
2084            server.set_allow_undeclared(node, false);
2085            assert!(!server.allows_undeclared(node), "raw {raw}");
2086        }
2087    }
2088
2089    /// The arena is shared, so capacity is an IMAGE bound, and the node that
2090    /// runs into it is told `StorageFull` rather than being silently given a
2091    /// sibling's slot.
2092    #[test]
2093    fn the_slot_arena_is_shared_across_nodes() {
2094        let a = NodeKey::new(0);
2095        let b = NodeKey::new(1);
2096        let mut storage = ParameterStorage::<2>::new();
2097        let mut server = ParameterServer::new_in(storage.as_table());
2098        assert!(server.declare(a, "one", ParameterValue::Integer(1)));
2099        assert!(server.declare(b, "one", ParameterValue::Integer(2)));
2100        assert!(server.is_full());
2101        assert!(!server.declare(a, "two", ParameterValue::Integer(3)));
2102        assert_eq!(server.total_len(), 2);
2103        assert_eq!(server.len(a), 1);
2104        assert_eq!(server.len(b), 1);
2105    }
2106
2107    /// `remove` takes the right node's slot.
2108    #[test]
2109    fn remove_is_keyed_by_node() {
2110        let a = NodeKey::new(0);
2111        let b = NodeKey::new(1);
2112        let mut storage: ParameterStorage = ParameterStorage::new();
2113        let mut server = ParameterServer::new_in(storage.as_table());
2114        assert!(server.declare(a, "x", ParameterValue::Integer(1)));
2115        assert!(server.declare(b, "x", ParameterValue::Integer(2)));
2116
2117        assert!(server.remove(a, "x"));
2118        assert!(!server.has(a, "x"));
2119        assert_eq!(server.get_integer(b, "x"), Some(2));
2120        assert_eq!(server.total_len(), 1);
2121    }
2122
2123    // ── issue 1150: step reaches `set`, and declare validates its range ──
2124
2125    #[test]
2126    fn set_rejects_a_value_off_the_integer_step() {
2127        let mut storage: ParameterStorage = ParameterStorage::new();
2128        let mut server = ParameterServer::new_in(storage.as_table());
2129        let desc = ParameterDescriptor::new("rate", ParameterType::Integer)
2130            .unwrap()
2131            .with_integer_range(0, 100, 10);
2132        assert!(server.declare_with_descriptor(
2133            NODE,
2134            "rate",
2135            ParameterValue::Integer(20),
2136            Some(desc)
2137        ));
2138
2139        assert_eq!(
2140            server.set(NODE, "rate", ParameterValue::Integer(30)),
2141            Ok(())
2142        );
2143        // In range by min/max, off the lattice: the old `contains` said yes.
2144        assert_eq!(
2145            server.set(NODE, "rate", ParameterValue::Integer(35)),
2146            Err(ParameterError::OutOfRange)
2147        );
2148        assert_eq!(server.get_integer(NODE, "rate"), Some(30));
2149    }
2150
2151    #[test]
2152    fn set_rejects_a_value_off_the_float_step() {
2153        let mut storage: ParameterStorage = ParameterStorage::new();
2154        let mut server = ParameterServer::new_in(storage.as_table());
2155        let desc = ParameterDescriptor::new("gain", ParameterType::Double)
2156            .unwrap()
2157            .with_float_range(0.0, 1.0, 0.25);
2158        assert!(server.declare_with_descriptor(
2159            NODE,
2160            "gain",
2161            ParameterValue::Double(0.5),
2162            Some(desc)
2163        ));
2164
2165        assert_eq!(server.set_double(NODE, "gain", 0.75), Ok(()));
2166        assert_eq!(
2167            server.set_double(NODE, "gain", 0.6),
2168            Err(ParameterError::OutOfRange)
2169        );
2170        assert_eq!(server.get_double(NODE, "gain"), Some(0.75));
2171    }
2172
2173    #[test]
2174    fn declare_refuses_an_ill_formed_range() {
2175        let mut storage: ParameterStorage = ParameterStorage::new();
2176        let mut server = ParameterServer::new_in(storage.as_table());
2177        let desc = ParameterDescriptor::new("bad", ParameterType::Integer)
2178            .unwrap()
2179            .with_integer_range(0, 100, -1);
2180        assert!(!server.declare_with_descriptor(
2181            NODE,
2182            "bad",
2183            ParameterValue::Integer(0),
2184            Some(desc.clone())
2185        ));
2186        assert!(!server.has(NODE, "bad"));
2187        assert_eq!(
2188            server.declare_parameter(NODE, desc, Some(&ParameterValue::Integer(0))),
2189            Err(ParameterError::InvalidRange)
2190        );
2191        assert!(!server.has(NODE, "bad"));
2192    }
2193
2194    #[test]
2195    fn declare_refuses_a_default_its_own_range_rejects() {
2196        let mut storage: ParameterStorage = ParameterStorage::new();
2197        let mut server = ParameterServer::new_in(storage.as_table());
2198        let desc = ParameterDescriptor::new("odd", ParameterType::Integer)
2199            .unwrap()
2200            .with_integer_range(0, 100, 2);
2201        assert!(!server.declare_with_descriptor(
2202            NODE,
2203            "odd",
2204            ParameterValue::Integer(3),
2205            Some(desc.clone())
2206        ));
2207        assert!(server.declare_with_descriptor(
2208            NODE,
2209            "odd",
2210            ParameterValue::Integer(4),
2211            Some(desc)
2212        ));
2213    }
2214
2215    // ── issue 1151: a wire-facing set on an undeclared name is refused ──
2216
2217    #[test]
2218    fn apply_rejects_an_undeclared_name_by_default() {
2219        let mut storage: ParameterStorage = ParameterStorage::new();
2220        let mut server = ParameterServer::new_in(storage.as_table());
2221        assert!(server.declare(NODE, "max_speed", ParameterValue::Double(1.0)));
2222        assert!(!server.allows_undeclared(NODE));
2223
2224        // The typo from the issue: reported success and created a parameter
2225        // nobody reads.
2226        assert_eq!(
2227            server.check_apply(NODE, "max_speeed", &ParameterValue::Double(5.0)),
2228            Err(ParameterError::Undeclared)
2229        );
2230        assert_eq!(
2231            server.apply(NODE, "max_speeed", ParameterValue::Double(5.0)),
2232            Err(ParameterError::Undeclared)
2233        );
2234        assert!(!server.has(NODE, "max_speeed"));
2235        assert_eq!(server.len(NODE), 1);
2236        assert_eq!(server.get_double(NODE, "max_speed"), Some(1.0));
2237
2238        // A declared name still sets.
2239        assert_eq!(
2240            server.apply(NODE, "max_speed", ParameterValue::Double(5.0)),
2241            Ok(())
2242        );
2243        assert_eq!(server.get_double(NODE, "max_speed"), Some(5.0));
2244    }
2245
2246    #[test]
2247    fn apply_declares_an_unknown_name_under_allow_undeclared() {
2248        let mut storage: ParameterStorage = ParameterStorage::new();
2249        let mut server = ParameterServer::new_in(storage.as_table());
2250        server.set_allow_undeclared(NODE, true);
2251        assert!(server.allows_undeclared(NODE));
2252
2253        assert_eq!(
2254            server.check_apply(NODE, "fresh", &ParameterValue::Integer(7)),
2255            Ok(())
2256        );
2257        assert_eq!(
2258            server.apply(NODE, "fresh", ParameterValue::Integer(7)),
2259            Ok(())
2260        );
2261        assert_eq!(server.get_integer(NODE, "fresh"), Some(7));
2262    }
2263
2264    #[test]
2265    fn apply_under_allow_undeclared_still_reports_a_full_store() {
2266        let mut storage: ParameterStorage<1> = ParameterStorage::new();
2267        let mut server = ParameterServer::new_in(storage.as_table());
2268        server.set_allow_undeclared(NODE, true);
2269        assert!(server.declare(NODE, "only", ParameterValue::Bool(true)));
2270        assert_eq!(
2271            server.check_apply(NODE, "second", &ParameterValue::Bool(true)),
2272            Err(ParameterError::StorageFull)
2273        );
2274        assert_eq!(
2275            server.apply(NODE, "second", ParameterValue::Bool(true)),
2276            Err(ParameterError::StorageFull)
2277        );
2278    }
2279
2280    #[test]
2281    fn apply_enforces_the_existing_entry_rules_through_the_same_predicate() {
2282        let mut storage: ParameterStorage = ParameterStorage::new();
2283        let mut server = ParameterServer::new_in(storage.as_table());
2284        let desc = ParameterDescriptor::new("rate", ParameterType::Integer)
2285            .unwrap()
2286            .with_integer_range(0, 100, 10);
2287        assert!(server.declare_with_descriptor(
2288            NODE,
2289            "rate",
2290            ParameterValue::Integer(20),
2291            Some(desc)
2292        ));
2293        assert_eq!(
2294            server.check_apply(NODE, "rate", &ParameterValue::Integer(35)),
2295            Err(ParameterError::OutOfRange)
2296        );
2297        assert_eq!(
2298            server.apply(NODE, "rate", ParameterValue::Integer(35)),
2299            Err(ParameterError::OutOfRange)
2300        );
2301        assert_eq!(
2302            server.apply(NODE, "rate", ParameterValue::Double(30.0)),
2303            Err(ParameterError::TypeMismatch)
2304        );
2305        assert_eq!(server.get_integer(NODE, "rate"), Some(20));
2306    }
2307
2308    #[test]
2309    fn test_new_server() {
2310        let mut storage: ParameterStorage = ParameterStorage::new();
2311        let server = ParameterServer::new_in(storage.as_table());
2312        assert_eq!(server.len(NODE), 0);
2313        assert!(server.is_empty(NODE));
2314    }
2315
2316    #[test]
2317    #[allow(clippy::approx_constant)]
2318    fn test_declare_and_get() {
2319        let mut storage: ParameterStorage = ParameterStorage::new();
2320        let mut server = ParameterServer::new_in(storage.as_table());
2321
2322        assert!(server.declare(NODE, "my_bool", ParameterValue::Bool(true)));
2323        assert!(server.declare(NODE, "my_int", ParameterValue::Integer(42)));
2324        assert!(server.declare(NODE, "my_double", ParameterValue::Double(3.14)));
2325
2326        assert_eq!(server.len(NODE), 3);
2327        assert!(!server.is_empty(NODE));
2328
2329        assert_eq!(server.get_bool(NODE, "my_bool"), Some(true));
2330        assert_eq!(server.get_integer(NODE, "my_int"), Some(42));
2331        assert_eq!(server.get_double(NODE, "my_double"), Some(3.14));
2332    }
2333
2334    #[test]
2335    fn test_unset_clears_the_value_and_reports_success() {
2336        // phase-359 W10 / issue 0080 — what survives of
2337        // ~~`test_dirty_tracks_value_changes_not_declarations`~~. That test existed
2338        // for the persistence flush's dirty flag, which is deleted with the
2339        // seam; but it carried the crate's ONLY coverage of `unset`, so the
2340        // assertion moves here rather than going out with the flag.
2341        let mut storage: ParameterStorage = ParameterStorage::new();
2342        let mut server = ParameterServer::new_in(storage.as_table());
2343        assert!(server.declare(NODE, "count", ParameterValue::Integer(0)));
2344        assert_eq!(server.set_integer(NODE, "count", 10), Ok(()));
2345        assert_eq!(server.get_integer(NODE, "count"), Some(10));
2346
2347        assert_eq!(server.unset(NODE, "count"), Ok(()));
2348        assert_eq!(server.get_integer(NODE, "count"), None);
2349    }
2350
2351    #[test]
2352    fn test_set_parameter() {
2353        let mut storage: ParameterStorage = ParameterStorage::new();
2354        let mut server = ParameterServer::new_in(storage.as_table());
2355        assert!(server.declare(NODE, "count", ParameterValue::Integer(0)));
2356
2357        assert_eq!(server.set_integer(NODE, "count", 10), Ok(()));
2358        assert_eq!(server.get_integer(NODE, "count"), Some(10));
2359    }
2360
2361    #[test]
2362    fn test_set_nonexistent() {
2363        let mut storage: ParameterStorage = ParameterStorage::new();
2364        let mut server = ParameterServer::new_in(storage.as_table());
2365        assert_eq!(
2366            server.set(NODE, "nonexistent", ParameterValue::Integer(1)),
2367            Err(ParameterError::NotFound)
2368        );
2369    }
2370
2371    #[test]
2372    fn test_read_only_parameter() {
2373        let mut storage: ParameterStorage = ParameterStorage::new();
2374        let mut server = ParameterServer::new_in(storage.as_table());
2375
2376        let desc = ParameterDescriptor::new("version", ParameterType::String)
2377            .unwrap()
2378            .with_read_only(true);
2379
2380        assert!(server.declare_with_descriptor(
2381            NODE,
2382            "version",
2383            ParameterValue::from_string("1.0.0").unwrap(),
2384            Some(desc),
2385        ));
2386
2387        assert_eq!(
2388            server.set_string(NODE, "version", "2.0.0"),
2389            Err(ParameterError::ReadOnly)
2390        );
2391        assert_eq!(server.get_string(NODE, "version"), Some("1.0.0"));
2392    }
2393
2394    #[test]
2395    fn test_range_constraints() {
2396        let mut storage: ParameterStorage = ParameterStorage::new();
2397        let mut server = ParameterServer::new_in(storage.as_table());
2398
2399        let desc = ParameterDescriptor::new("speed", ParameterType::Double)
2400            .unwrap()
2401            .with_float_range(0.0, 10.0, 0.0);
2402
2403        assert!(server.declare_with_descriptor(
2404            NODE,
2405            "speed",
2406            ParameterValue::Double(5.0),
2407            Some(desc)
2408        ));
2409
2410        // Valid value
2411        assert_eq!(server.set_double(NODE, "speed", 8.0), Ok(()));
2412
2413        // Out of range
2414        assert_eq!(
2415            server.set_double(NODE, "speed", 15.0),
2416            Err(ParameterError::OutOfRange)
2417        );
2418        assert_eq!(server.get_double(NODE, "speed"), Some(8.0)); // Unchanged
2419    }
2420
2421    #[test]
2422    fn test_remove_parameter() {
2423        let mut storage: ParameterStorage = ParameterStorage::new();
2424        let mut server = ParameterServer::new_in(storage.as_table());
2425        assert!(server.declare(NODE, "temp", ParameterValue::Double(25.0)));
2426
2427        assert!(server.has(NODE, "temp"));
2428        assert!(server.remove(NODE, "temp"));
2429        assert!(!server.has(NODE, "temp"));
2430        assert!(!server.remove(NODE, "temp")); // Already removed
2431    }
2432
2433    #[test]
2434    fn test_list_parameters() {
2435        let mut storage: ParameterStorage = ParameterStorage::new();
2436        let mut server = ParameterServer::new_in(storage.as_table());
2437        assert!(server.declare(NODE, "robot.speed", ParameterValue::Double(1.0)));
2438        assert!(server.declare(
2439            NODE,
2440            "robot.name",
2441            ParameterValue::from_string("bot1").unwrap(),
2442        ));
2443        assert!(server.declare(NODE, "sensor.range", ParameterValue::Double(10.0)));
2444
2445        let robot_params: heapless::Vec<&str, 8> =
2446            server.list_with_prefix(NODE, "robot.").collect();
2447        assert_eq!(robot_params.len(), 2);
2448    }
2449
2450    #[test]
2451    fn test_set_or_declare() {
2452        let mut storage: ParameterStorage = ParameterStorage::new();
2453        let mut server = ParameterServer::new_in(storage.as_table());
2454
2455        // First call declares
2456        assert_eq!(
2457            server.set_or_declare(NODE, "new_param", ParameterValue::Integer(1)),
2458            Ok(())
2459        );
2460        assert_eq!(server.get_integer(NODE, "new_param"), Some(1));
2461
2462        // Second call sets
2463        assert_eq!(
2464            server.set_or_declare(NODE, "new_param", ParameterValue::Integer(2)),
2465            Ok(())
2466        );
2467        assert_eq!(server.get_integer(NODE, "new_param"), Some(2));
2468    }
2469
2470    #[test]
2471    fn test_duplicate_declare() {
2472        let mut storage: ParameterStorage = ParameterStorage::new();
2473        let mut server = ParameterServer::new_in(storage.as_table());
2474        assert!(server.declare(NODE, "param", ParameterValue::Integer(1)));
2475        assert!(!server.declare(NODE, "param", ParameterValue::Integer(2))); // Already exists
2476        assert_eq!(server.get_integer(NODE, "param"), Some(1)); // Unchanged
2477    }
2478
2479    // ── phase-417 W4.a: descriptors, undeclare, and the on-set hook ──
2480
2481    /// The four rclc descriptor verbs land on the ONE store and are visible
2482    /// through the ordinary descriptor read — a C or C++ wrapper forwarding to
2483    /// these adds no table of its own.
2484    #[test]
2485    // The comparison and the `min` below read as "always true" to clippy on a
2486    // build that leaves `NROS_MAX_PARAM_CONSTRAINTS_LEN` at its default of 0 --
2487    // and that is the point: the constant is a KNOB, the test is written
2488    // against it so it holds at 0, at 64 and at 256, and a version specialised
2489    // to whichever value this build resolved would read as coverage on the
2490    // other two.
2491    #[allow(clippy::absurd_extreme_comparisons, clippy::unnecessary_min_or_max)]
2492    fn descriptor_verbs_attach_to_a_declared_parameter() {
2493        let mut storage: ParameterStorage = ParameterStorage::new();
2494        let mut server = ParameterServer::new_in(storage.as_table());
2495        assert!(server.declare(NODE, "rate", ParameterValue::Double(2.0)));
2496
2497        // A parameter declared with no descriptor still gets one on demand:
2498        // rclc's order is declare-then-describe.
2499        assert!(server.get_descriptor(NODE, "rate").is_none());
2500        assert!(server.set_parameter_description(NODE, "rate", "publish rate", "hz, positive"));
2501        assert!(server.set_parameter_float_range(NODE, "rate", 0.0, 10.0, 0.0));
2502
2503        let desc = server.get_descriptor(NODE, "rate").expect("descriptor");
2504        assert_eq!(desc.description.as_str(), "publish rate");
2505        // Written against the CAPACITY, not a literal: `additional_constraints`
2506        // has its own knob (`NROS_MAX_PARAM_CONSTRAINTS_LEN`) whose default is
2507        // 0, so the default build stores a zero-length prefix. A test that
2508        // asserted the whole string would pass only on a build that states the
2509        // knob, and would read as coverage everywhere else.
2510        let constraints_fit = "hz, positive".len() <= crate::MAX_PARAM_CONSTRAINTS_LEN;
2511        assert_eq!(
2512            desc.additional_constraints.as_str(),
2513            &"hz, positive"[..crate::MAX_PARAM_CONSTRAINTS_LEN.min("hz, positive".len())]
2514        );
2515        assert_eq!(desc.param_type, ParameterType::Double);
2516
2517        // And the range is ENFORCED by the same predicate every set uses.
2518        assert_eq!(
2519            server.apply(NODE, "rate", ParameterValue::Double(50.0)),
2520            Err(ParameterError::OutOfRange)
2521        );
2522        assert_eq!(
2523            server.apply(NODE, "rate", ParameterValue::Double(5.0)),
2524            Ok(())
2525        );
2526
2527        // read-only is the same shape.
2528        assert!(server.set_parameter_read_only(NODE, "rate", true));
2529        assert_eq!(
2530            server.apply(NODE, "rate", ParameterValue::Double(6.0)),
2531            Err(ParameterError::ReadOnly)
2532        );
2533
2534        // Text that did not fit is REPORTED, never dropped in silence — which
2535        // is what makes a capacity of 0 an honest default rather than a
2536        // trap. At a capacity that fits, nothing is reported.
2537        let mut reported = 0usize;
2538        server.take_truncated_descriptions(|_, _| reported += 1);
2539        assert_eq!(
2540            reported,
2541            usize::from(!constraints_fit),
2542            "a constraints string that did not fit the capacity went unreported"
2543        );
2544
2545        // An undeclared name is refused rather than silently creating a
2546        // descriptor for a parameter that is not there.
2547        assert!(!server.set_parameter_description(NODE, "absent", "d", "c"));
2548        assert!(!server.set_parameter_read_only(NODE, "absent", true));
2549        assert!(!server.set_parameter_integer_range(NODE, "absent", 0, 1, 1));
2550    }
2551
2552    /// A range the stored value already violates is REFUSED, and so is an
2553    /// ill-formed one. Attaching either would advertise a bound over a value
2554    /// no `set` could have produced.
2555    #[test]
2556    fn a_range_must_fit_the_value_it_is_attached_to() {
2557        let mut storage: ParameterStorage = ParameterStorage::new();
2558        let mut server = ParameterServer::new_in(storage.as_table());
2559        assert!(server.declare(NODE, "depth", ParameterValue::Integer(100)));
2560
2561        assert!(!server.set_parameter_integer_range(NODE, "depth", 0, 10, 1));
2562        assert!(server.get_descriptor(NODE, "depth").is_none());
2563
2564        // min > max
2565        assert!(!server.set_parameter_integer_range(NODE, "depth", 10, 0, 1));
2566        // negative step
2567        assert!(!server.set_parameter_integer_range(NODE, "depth", 0, 1000, -1));
2568
2569        assert!(server.set_parameter_integer_range(NODE, "depth", 0, 1000, 1));
2570    }
2571
2572    /// `remove` is the undeclare every language forwards to: the slot frees,
2573    /// the count drops, and a later declare of the same name succeeds.
2574    #[test]
2575    fn undeclare_frees_the_slot_for_that_node_only() {
2576        let mut storage: ParameterStorage = ParameterStorage::new();
2577        let mut server = ParameterServer::new_in(storage.as_table());
2578        let other = NodeKey::new(1);
2579        assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2580        assert!(server.declare(other, "rate", ParameterValue::Integer(2)));
2581        assert_eq!(server.total_len(), 2);
2582
2583        assert!(server.remove(NODE, "rate"));
2584        assert!(!server.has(NODE, "rate"));
2585        assert!(server.has(other, "rate"));
2586        assert_eq!(server.total_len(), 1);
2587        assert!(!server.remove(NODE, "rate"));
2588
2589        assert!(server.declare(NODE, "rate", ParameterValue::Integer(3)));
2590        assert_eq!(server.get_integer(NODE, "rate"), Some(3));
2591    }
2592
2593    fn refuse_everything(
2594        _n: NodeKey,
2595        _name: &str,
2596        _old: Option<&ParameterValue>,
2597        _new: Option<&ParameterValue>,
2598        _ctx: OnSetContext,
2599    ) -> bool {
2600        false
2601    }
2602
2603    fn refuse_if_ctx_matches(
2604        _n: NodeKey,
2605        _name: &str,
2606        _old: Option<&ParameterValue>,
2607        new: Option<&ParameterValue>,
2608        ctx: OnSetContext,
2609    ) -> bool {
2610        new.and_then(ParameterValue::as_integer) != Some(ctx.a as i64)
2611    }
2612
2613    /// The hook runs on `apply` — the seam every facade and the six services
2614    /// funnel through — refuses with `Rejected`, leaves the value alone, and
2615    /// only for the node it was registered on.
2616    #[test]
2617    fn an_on_set_callback_vetoes_the_write_it_refuses() {
2618        let mut storage: ParameterStorage = ParameterStorage::new();
2619        let mut server = ParameterServer::new_in(storage.as_table());
2620        let other = NodeKey::new(1);
2621        assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2622        assert!(server.declare(other, "rate", ParameterValue::Integer(1)));
2623
2624        let h = server
2625            .add_on_set_parameters_callback(NODE, refuse_everything, OnSetContext::default())
2626            .expect("a free slot");
2627
2628        assert_eq!(
2629            server.apply(NODE, "rate", ParameterValue::Integer(9)),
2630            Err(ParameterError::Rejected)
2631        );
2632        assert_eq!(server.get_integer(NODE, "rate"), Some(1));
2633        // A sibling node's writes are untouched: the registration names a node.
2634        assert_eq!(
2635            server.apply(other, "rate", ParameterValue::Integer(9)),
2636            Ok(())
2637        );
2638
2639        assert!(server.remove_on_set_parameters_callback(h));
2640        assert!(!server.remove_on_set_parameters_callback(h));
2641        assert_eq!(
2642            server.apply(NODE, "rate", ParameterValue::Integer(9)),
2643            Ok(())
2644        );
2645    }
2646
2647    /// What the recording hook saw, reached through `OnSetContext::a`.
2648    #[derive(Default)]
2649    struct Seen {
2650        calls: u32,
2651        old: Option<i64>,
2652        new: Option<i64>,
2653        old_present: bool,
2654        new_present: bool,
2655    }
2656
2657    fn record(
2658        _n: NodeKey,
2659        _name: &str,
2660        old: Option<&ParameterValue>,
2661        new: Option<&ParameterValue>,
2662        ctx: OnSetContext,
2663    ) -> bool {
2664        // SAFETY: every test passes a `&mut Seen` that outlives the server.
2665        let seen = unsafe { &mut *(ctx.a as *mut Seen) };
2666        seen.calls += 1;
2667        seen.old_present = old.is_some();
2668        seen.new_present = new.is_some();
2669        seen.old = old.and_then(ParameterValue::as_integer);
2670        seen.new = new.and_then(ParameterValue::as_integer);
2671        true
2672    }
2673
2674    /// phase-491 W3 — the `/parameter_events` log: off by default, one entry
2675    /// per name (merged), and a burst past its bound is COUNTED, never blocked.
2676    #[test]
2677    fn the_event_log_merges_by_name_and_counts_overflow() {
2678        let mut storage: ParameterStorage = ParameterStorage::new();
2679        let mut server = ParameterServer::new_in(storage.as_table());
2680        assert!(server.declare(NODE, "quiet", ParameterValue::Integer(0)));
2681        assert!(server.pop_parameter_event().is_none(), "off by default");
2682
2683        server.set_parameter_events(NODE, true);
2684        // Issue 2023 — the start-up event names what the node already holds.
2685        let e = server
2686            .pop_initial_parameter_event()
2687            .expect("the start-up event");
2688        assert_eq!(
2689            (e.name.as_str(), e.kind),
2690            ("quiet", ParameterEventKind::New)
2691        );
2692        assert!(server.pop_initial_parameter_event().is_none());
2693        assert!(server.declare(NODE, "a", ParameterValue::Integer(1)));
2694        assert_eq!(server.set(NODE, "a", ParameterValue::Integer(2)), Ok(()));
2695        assert!(server.remove(NODE, "a"));
2696        let e = server.pop_parameter_event().expect("one merged entry");
2697        assert_eq!(
2698            (e.name.as_str(), e.kind),
2699            ("a", ParameterEventKind::Deleted)
2700        );
2701        assert!(server.pop_parameter_event().is_none());
2702
2703        for i in 0..PARAMETER_EVENT_LOG_LEN + 2 {
2704            let mut name = heapless::String::<8>::new();
2705            core::fmt::Write::write_fmt(&mut name, format_args!("p{i}")).unwrap();
2706            assert!(server.declare(NODE, &name, ParameterValue::Integer(0)));
2707        }
2708        assert_eq!(server.take_parameter_events_dropped(), 2);
2709        assert_eq!(
2710            server.take_parameter_events_dropped(),
2711            0,
2712            "the count resets"
2713        );
2714    }
2715
2716    /// Issue 2023 — the start-up event: one `New` per parameter the node held
2717    /// when events went on, at its CURRENT value; an `Announce` for a node
2718    /// holding none; changes made while it is pending fold into it rather
2719    /// than doubling up in the log; and re-enabling is not a second start-up.
2720    #[test]
2721    fn the_start_up_event_reports_what_the_node_holds() {
2722        let mut storage: ParameterStorage = ParameterStorage::new();
2723        let mut server = ParameterServer::new_in(storage.as_table());
2724        let other = NodeKey::new(1);
2725        assert!(server.declare(NODE, "a", ParameterValue::Integer(1)));
2726        assert!(server.declare(NODE, "b", ParameterValue::Integer(2)));
2727        assert!(server.declare(other, "x", ParameterValue::Integer(9)));
2728
2729        server.set_parameter_events(NODE, true);
2730        server.set_parameter_events(NodeKey::new(2), true);
2731        // Pending: a declaration and a removal fold into the drain.
2732        assert!(server.declare(NODE, "c", ParameterValue::Integer(3)));
2733        assert!(server.remove(NODE, "b"));
2734        assert!(server.pop_parameter_event().is_none(), "not logged twice");
2735
2736        let mut seen = heapless::Vec::<(u8, ParameterEventKind, String<8>), 8>::new();
2737        while let Some(e) = server.pop_initial_parameter_event() {
2738            let mut n = String::new();
2739            n.push_str(e.name.as_str()).unwrap();
2740            seen.push((e.node.raw(), e.kind, n)).unwrap();
2741        }
2742        let names: heapless::Vec<(u8, ParameterEventKind, &str), 8> =
2743            seen.iter().map(|(n, k, s)| (*n, *k, s.as_str())).collect();
2744        assert_eq!(
2745            names.as_slice(),
2746            &[
2747                (0, ParameterEventKind::New, "a"),
2748                (0, ParameterEventKind::New, "c"),
2749                (2, ParameterEventKind::Announce, ""),
2750            ],
2751            "node 1 never enabled events; node 2 holds nothing"
2752        );
2753
2754        // Already on: a second enable is not a second start-up.
2755        server.set_parameter_events(NODE, true);
2756        assert!(server.pop_initial_parameter_event().is_none());
2757        // After it drained, changes are logged as before.
2758        assert!(server.remove(NODE, "a"));
2759        let e = server.pop_parameter_event().expect("a logged deletion");
2760        assert_eq!(
2761            (e.name.as_str(), e.kind),
2762            ("a", ParameterEventKind::Deleted)
2763        );
2764    }
2765
2766    /// phase-491 W2 — the hook sees BOTH sides: `(old, new)` on a write to an
2767    /// existing parameter, `(None, new)` on a remote create.
2768    #[test]
2769    fn the_on_set_hook_sees_old_and_new() {
2770        let mut storage: ParameterStorage = ParameterStorage::new();
2771        let mut server = ParameterServer::new_in(storage.as_table());
2772        let mut seen = Seen::default();
2773        assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2774        server
2775            .add_on_set_parameters_callback(NODE, record, OnSetContext::new(&raw mut seen as usize))
2776            .expect("a free slot");
2777
2778        assert_eq!(
2779            server.apply(NODE, "rate", ParameterValue::Integer(2)),
2780            Ok(())
2781        );
2782        assert_eq!((seen.old, seen.new), (Some(1), Some(2)));
2783
2784        server.set_allow_undeclared(NODE, true);
2785        assert_eq!(
2786            server.apply(NODE, "fresh", ParameterValue::Integer(7)),
2787            Ok(())
2788        );
2789        assert!(!seen.old_present, "a create has no old value");
2790        assert_eq!(seen.new, Some(7));
2791    }
2792
2793    /// phase-491 W2 — rclc's LOCAL set: type, hook, write. Read-only and range
2794    /// do not bind the owner; a different type does.
2795    #[test]
2796    fn a_local_set_skips_read_only_and_range_but_not_type() {
2797        let mut storage: ParameterStorage = ParameterStorage::new();
2798        let mut server = ParameterServer::new_in(storage.as_table());
2799        let mut seen = Seen::default();
2800        assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2801        assert!(server.set_parameter_integer_range(NODE, "rate", 0, 10, 1));
2802        assert!(server.set_parameter_read_only(NODE, "rate", true));
2803        server
2804            .add_on_set_parameters_callback(NODE, record, OnSetContext::new(&raw mut seen as usize))
2805            .expect("a free slot");
2806
2807        // The remote path refuses both.
2808        assert_eq!(
2809            server.apply(NODE, "rate", ParameterValue::Integer(50)),
2810            Err(ParameterError::ReadOnly)
2811        );
2812        assert_eq!(seen.calls, 0, "a refused remote write reached the hook");
2813
2814        // The local path does not, and the hook sees (old, new).
2815        assert_eq!(
2816            server.set_local(NODE, "rate", ParameterValue::Integer(50)),
2817            Ok(())
2818        );
2819        assert_eq!((seen.old, seen.new), (Some(1), Some(50)));
2820        assert_eq!(server.get_integer(NODE, "rate"), Some(50));
2821
2822        assert_eq!(
2823            server.set_local(NODE, "rate", ParameterValue::Double(1.0)),
2824            Err(ParameterError::TypeMismatch)
2825        );
2826        assert_eq!(
2827            server.set_local(NODE, "nope", ParameterValue::Integer(1)),
2828            Err(ParameterError::NotFound)
2829        );
2830        assert!(
2831            server
2832                .add_on_set_parameters_callback(NODE, refuse_everything, OnSetContext::default())
2833                .is_some()
2834        );
2835        assert_eq!(
2836            server.set_local(NODE, "rate", ParameterValue::Integer(3)),
2837            Err(ParameterError::Rejected)
2838        );
2839        assert_eq!(server.get_integer(NODE, "rate"), Some(50));
2840    }
2841
2842    /// phase-491 W2 — rclc's set service DELETES on `NOT_SET`, after the hook
2843    /// accepts `(old, None)`; rclcpp's refuses. Per node.
2844    #[test]
2845    fn not_set_deletes_only_on_a_node_that_asked() {
2846        let mut storage: ParameterStorage = ParameterStorage::new();
2847        let mut server = ParameterServer::new_in(storage.as_table());
2848        let other = NodeKey::new(1);
2849        let mut seen = Seen::default();
2850        assert!(server.declare(NODE, "rate", ParameterValue::Integer(4)));
2851        assert!(server.declare(other, "rate", ParameterValue::Integer(4)));
2852        server.set_delete_on_not_set(NODE, true);
2853        server
2854            .add_on_set_parameters_callback(NODE, record, OnSetContext::new(&raw mut seen as usize))
2855            .expect("a free slot");
2856
2857        assert_eq!(server.apply(NODE, "rate", ParameterValue::NotSet), Ok(()));
2858        assert!(!server.has(NODE, "rate"));
2859        assert_eq!((seen.old, seen.new_present), (Some(4), false));
2860
2861        // The other node keeps rclcpp's semantics: no delete (with no descriptor
2862        // the store just records NOT_SET, as it always has).
2863        let _ = server.apply(other, "rate", ParameterValue::NotSet);
2864        assert!(
2865            server.has(other, "rate"),
2866            "the rclcpp node's parameter was deleted"
2867        );
2868
2869        assert!(server.declare(NODE, "ro", ParameterValue::Integer(1)));
2870        assert!(server.set_parameter_read_only(NODE, "ro", true));
2871        assert_eq!(
2872            server.apply(NODE, "ro", ParameterValue::NotSet),
2873            Err(ParameterError::ReadOnly)
2874        );
2875        assert!(server.has(NODE, "ro"));
2876    }
2877
2878    /// Issue 1812 / RFC-0105 D10.2 — `apply_staged` is `apply` with the value
2879    /// taken from a slot: the same verdict for every row `apply` decides, and
2880    /// the same store afterwards. Each row runs on two fresh, identically
2881    /// prepared servers, one per entry point.
2882    #[test]
2883    fn apply_staged_is_apply_from_a_slot() {
2884        fn reject(
2885            _: NodeKey,
2886            name: &str,
2887            _: Option<&ParameterValue>,
2888            _: Option<&ParameterValue>,
2889            _: OnSetContext,
2890        ) -> bool {
2891            name != "vetoed"
2892        }
2893        fn prepared(storage: &mut ParameterStorage<4>) -> ParameterServer<'_> {
2894            let mut server = ParameterServer::new_in(storage.as_table());
2895            assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2896            assert!(
2897                server.declare_with_descriptor(
2898                    NODE,
2899                    "ro",
2900                    ParameterValue::Integer(1),
2901                    Some(
2902                        ParameterDescriptor::new("ro", ParameterType::Integer)
2903                            .unwrap()
2904                            .with_read_only(true)
2905                    ),
2906                )
2907            );
2908            assert!(server.declare_with_descriptor(
2909                NODE,
2910                "typed",
2911                ParameterValue::Integer(1),
2912                Some(ParameterDescriptor::new("typed", ParameterType::Integer).unwrap()),
2913            ));
2914            assert!(server.declare(NODE, "vetoed", ParameterValue::Integer(1)));
2915            server
2916                .add_on_set_parameters_callback(NODE, reject, OnSetContext::new(0))
2917                .expect("a free slot");
2918            server
2919        }
2920        // The value is built per row (a `fn`), not held in the table: a
2921        // `ParameterValue` is ~8.5 KiB, which is the point of the slot.
2922        type Row = (&'static str, fn() -> ParameterValue, bool, bool);
2923        let rows: [Row; 7] = [
2924            ("rate", || ParameterValue::Integer(7), false, false),
2925            ("ro", || ParameterValue::Integer(7), false, false),
2926            ("typed", || ParameterValue::Bool(true), false, false),
2927            ("vetoed", || ParameterValue::Integer(7), false, false),
2928            ("fresh", || ParameterValue::Integer(7), false, false),
2929            // allow_undeclared: declares, but the 4-slot table is full.
2930            ("fresh", || ParameterValue::Integer(7), true, false),
2931            // delete_on_not_set: the rclc remote delete.
2932            ("rate", || ParameterValue::NotSet, false, true),
2933        ];
2934        for (name, value, allow, delete) in rows {
2935            let mut a_storage: ParameterStorage<4> = ParameterStorage::new();
2936            let mut b_storage: ParameterStorage<4> = ParameterStorage::new();
2937            let mut by_value = prepared(&mut a_storage);
2938            let mut by_slot = prepared(&mut b_storage);
2939            for s in [&mut by_value, &mut by_slot] {
2940                s.set_allow_undeclared(NODE, allow);
2941                s.set_delete_on_not_set(NODE, delete);
2942            }
2943            let want = by_value.apply(NODE, name, value());
2944            let mut slot = value();
2945            let got = by_slot.apply_staged(NODE, name, &mut slot);
2946            assert_eq!(got, want, "verdict for `{name}`");
2947            for probe in ["rate", "ro", "typed", "vetoed", "fresh"] {
2948                assert_eq!(
2949                    by_slot.get(NODE, probe).map(ParameterValue::param_type),
2950                    by_value.get(NODE, probe).map(ParameterValue::param_type),
2951                    "`{probe}` after setting `{name}`"
2952                );
2953                assert_eq!(
2954                    by_slot.get_integer(NODE, probe),
2955                    by_value.get_integer(NODE, probe),
2956                    "`{probe}` after setting `{name}`"
2957                );
2958            }
2959        }
2960
2961        // The undeclared add, with room: declared, value swapped in.
2962        let mut storage: ParameterStorage<4> = ParameterStorage::new();
2963        let mut server = ParameterServer::new_in(storage.as_table());
2964        server.set_allow_undeclared(NODE, true);
2965        let mut slot = ParameterValue::Double(2.5);
2966        assert_eq!(server.apply_staged(NODE, "fresh", &mut slot), Ok(()));
2967        assert_eq!(server.get_double(NODE, "fresh"), Some(2.5));
2968        assert_eq!(server.len(NODE), 1);
2969
2970        // A SWAP: the slot comes back holding the value it displaced.
2971        let mut slot = ParameterValue::Double(4.0);
2972        assert!(server.apply_staged(NODE, "fresh", &mut slot).is_ok());
2973        assert_eq!(slot.as_double(), Some(2.5));
2974        assert_eq!(server.get_double(NODE, "fresh"), Some(4.0));
2975    }
2976
2977    /// The registry is BOUNDED and says so: the (N+1)th registration is `None`,
2978    /// never a silent eviction of somebody else's hook. The context reaches
2979    /// the callback.
2980    #[test]
2981    fn the_on_set_registry_refuses_past_its_stated_capacity() {
2982        let mut storage: ParameterStorage = ParameterStorage::new();
2983        let mut server = ParameterServer::new_in(storage.as_table());
2984        assert!(server.declare(NODE, "rate", ParameterValue::Integer(1)));
2985
2986        assert_eq!(server.on_set_callbacks_free(), MAX_ON_SET_CALLBACKS);
2987        let mut handles = heapless::Vec::<_, MAX_ON_SET_CALLBACKS>::new();
2988        for i in 0..MAX_ON_SET_CALLBACKS {
2989            let h = server
2990                .add_on_set_parameters_callback(
2991                    NODE,
2992                    refuse_if_ctx_matches,
2993                    OnSetContext::new(40 + i),
2994                )
2995                .expect("within capacity");
2996            handles.push(h).expect("bounded by the same constant");
2997        }
2998        assert_eq!(server.on_set_callbacks_free(), 0);
2999        assert!(
3000            server
3001                .add_on_set_parameters_callback(NODE, refuse_everything, OnSetContext::default())
3002                .is_none()
3003        );
3004
3005        // Every registered hook is consulted, and each got its own context.
3006        for i in 0..MAX_ON_SET_CALLBACKS {
3007            assert_eq!(
3008                server.apply(NODE, "rate", ParameterValue::Integer(40 + i as i64)),
3009                Err(ParameterError::Rejected)
3010            );
3011        }
3012        assert_eq!(
3013            server.apply(NODE, "rate", ParameterValue::Integer(7)),
3014            Ok(())
3015        );
3016
3017        assert!(server.remove_on_set_parameters_callback(handles[0]));
3018        assert_eq!(server.on_set_callbacks_free(), 1);
3019    }
3020
3021    /// The hook runs AFTER the store's own rules, so a callback never sees a
3022    /// write the descriptor already refused.
3023    #[test]
3024    fn the_store_rules_run_before_the_hook() {
3025        let mut storage: ParameterStorage = ParameterStorage::new();
3026        let mut server = ParameterServer::new_in(storage.as_table());
3027        let desc = ParameterDescriptor::new("rate", ParameterType::Integer)
3028            .expect("name fits")
3029            .with_read_only(true);
3030        assert!(server.declare_with_descriptor(
3031            NODE,
3032            "rate",
3033            ParameterValue::Integer(1),
3034            Some(desc)
3035        ));
3036        let _ = server
3037            .add_on_set_parameters_callback(NODE, refuse_everything, OnSetContext::default())
3038            .expect("a free slot");
3039        // ReadOnly, not Rejected: the callback was never consulted.
3040        assert_eq!(
3041            server.apply(NODE, "rate", ParameterValue::Integer(2)),
3042            Err(ParameterError::ReadOnly)
3043        );
3044    }
3045
3046    /// The registry's cost is MEASURED, not asserted in prose: the doc on
3047    /// [`MAX_ON_SET_CALLBACKS`] names a byte count, and a slot that grows
3048    /// silently is how that number goes stale (the issue-1022 class).
3049    #[test]
3050    fn the_on_set_registry_costs_what_its_doc_says() {
3051        let slot = core::mem::size_of::<Option<OnSetCallbackSlot>>();
3052        let word = core::mem::size_of::<usize>();
3053        // node (1 byte, padded) + fn pointer + two context words, and the
3054        // `Option` rides in the padding or the null fn pointer.
3055        assert!(
3056            slot <= 4 * word,
3057            "an on-set slot is {slot} bytes, more than the four words its doc claims"
3058        );
3059        assert_eq!(
3060            core::mem::size_of::<[Option<OnSetCallbackSlot>; MAX_ON_SET_CALLBACKS]>(),
3061            MAX_ON_SET_CALLBACKS * slot
3062        );
3063    }
3064
3065    /// `try_declare` says WHY, where `declare` said only `false`.
3066    #[test]
3067    fn try_declare_names_the_refusal() {
3068        let mut storage: ParameterStorage<2> = ParameterStorage::new();
3069        let mut server = ParameterServer::new_in(storage.as_table());
3070
3071        assert_eq!(
3072            server.try_declare(NODE, "a", ParameterValue::Integer(1)),
3073            Ok(())
3074        );
3075        assert_eq!(
3076            server.try_declare(NODE, "a", ParameterValue::Integer(1)),
3077            Err(DeclarationError::AlreadyDeclared)
3078        );
3079
3080        let bad_range = ParameterDescriptor::new("b", ParameterType::Integer)
3081            .expect("name fits")
3082            .with_integer_range(10, 0, 1);
3083        assert_eq!(
3084            server.try_declare_with_descriptor(
3085                NODE,
3086                "b",
3087                ParameterValue::Integer(1),
3088                Some(bad_range)
3089            ),
3090            Err(DeclarationError::InvalidRange)
3091        );
3092
3093        let out_of_range = ParameterDescriptor::new("b", ParameterType::Integer)
3094            .expect("name fits")
3095            .with_integer_range(0, 10, 1);
3096        assert_eq!(
3097            server.try_declare_with_descriptor(
3098                NODE,
3099                "b",
3100                ParameterValue::Integer(99),
3101                Some(out_of_range)
3102            ),
3103            Err(DeclarationError::InitialValueOutOfRange)
3104        );
3105
3106        assert_eq!(
3107            server.try_declare(NODE, "b", ParameterValue::Integer(1)),
3108            Ok(())
3109        );
3110        assert_eq!(
3111            server.try_declare(NODE, "c", ParameterValue::Integer(1)),
3112            Err(DeclarationError::StorageFull)
3113        );
3114
3115        let long = "n".repeat(MAX_PARAM_NAME_LEN + 1);
3116        let mut roomy: ParameterStorage<2> = ParameterStorage::new();
3117        let mut server = ParameterServer::new_in(roomy.as_table());
3118        assert_eq!(
3119            server.try_declare(NODE, &long, ParameterValue::Integer(1)),
3120            Err(DeclarationError::NameTooLong)
3121        );
3122    }
3123}
3124
3125// =============================================================================
3126// Ghost model validation
3127// =============================================================================
3128
3129#[cfg(test)]
3130mod ghost_checks {
3131    use super::*;
3132    use nros_ghost_types::ParamServerGhost;
3133
3134    /// phase-426 W1 — the node these single-node tests are about.
3135    const NODE: NodeKey = NodeKey::PRIMARY;
3136
3137    /// Structural check: construct ParamServerGhost from ParameterServer private fields.
3138    /// If a field is renamed or retyped, this fails to compile.
3139    fn ghost_from_server(s: &ParameterServer<'_>) -> ParamServerGhost {
3140        ParamServerGhost {
3141            count: s.count,
3142            // phase-382 W2' — capacity is the borrowed TABLE's length. It used
3143            // to read `MAX_PARAMETERS`, which was the same number only because
3144            // the storage was the knob-sized array inside the server.
3145            max: s.table.capacity(),
3146        }
3147    }
3148
3149    #[test]
3150    fn ghost_new_count() {
3151        let mut storage: ParameterStorage = ParameterStorage::new();
3152        let capacity = storage.capacity();
3153        let server = ParameterServer::new_in(storage.as_table());
3154        let ghost = ghost_from_server(&server);
3155        assert_eq!(ghost.count, 0);
3156        // phase-382 W2' — the ghost's `max` is the TABLE's length, and the
3157        // default-parameter `ParameterStorage` is `MAX_PARAMETERS` long. This
3158        // used to be a literal `32`, which held only while the knob was at its
3159        // default.
3160        assert_eq!(ghost.max, capacity);
3161        assert_eq!(ghost.max, MAX_PARAMETERS);
3162    }
3163
3164    /// phase-382 W2' — capacity follows the TABLE, not `MAX_PARAMETERS`: a
3165    /// server built on a short table is full at that table's length.
3166    #[test]
3167    fn ghost_max_follows_a_shorter_table() {
3168        let mut storage = ParameterStorage::<2>::new();
3169        let mut server = ParameterServer::new_in(storage.as_table());
3170        assert_eq!(ghost_from_server(&server).max, 2);
3171        assert!(server.declare(NODE, "a", ParameterValue::Integer(1)));
3172        assert!(server.declare(NODE, "b", ParameterValue::Integer(2)));
3173        assert!(server.is_full());
3174        assert!(!server.declare(NODE, "c", ParameterValue::Integer(3)));
3175        let ghost = ghost_from_server(&server);
3176        assert!(ghost.count <= ghost.max);
3177    }
3178
3179    /// phase-382 W2' — `new_in` ADOPTS the table it is handed, so a second
3180    /// server over the same storage sees the parameters the first declared
3181    /// and keeps its count in step with the slots.
3182    #[test]
3183    fn ghost_count_recovers_from_a_reused_table() {
3184        let mut storage = ParameterStorage::<4>::new();
3185        {
3186            let mut first = ParameterServer::new_in(storage.as_table());
3187            assert!(first.declare(NODE, "kept", ParameterValue::Integer(7)));
3188        }
3189        let second = ParameterServer::new_in(storage.as_table());
3190        assert_eq!(ghost_from_server(&second).count, 1);
3191        assert_eq!(second.get_integer(NODE, "kept"), Some(7));
3192    }
3193
3194    #[test]
3195    fn ghost_declare_increments() {
3196        let mut storage: ParameterStorage = ParameterStorage::new();
3197        let mut server = ParameterServer::new_in(storage.as_table());
3198        let before = ghost_from_server(&server).count;
3199        assert!(server.declare(NODE, "test", ParameterValue::Integer(1)));
3200        let after = ghost_from_server(&server).count;
3201        assert_eq!(after, before + 1);
3202    }
3203
3204    #[test]
3205    fn ghost_remove_decrements() {
3206        let mut storage: ParameterStorage = ParameterStorage::new();
3207        let mut server = ParameterServer::new_in(storage.as_table());
3208        assert!(server.declare(NODE, "test", ParameterValue::Integer(1)));
3209        let before = ghost_from_server(&server).count;
3210        assert!(server.remove(NODE, "test"));
3211        let after = ghost_from_server(&server).count;
3212        assert_eq!(after, before - 1);
3213    }
3214
3215    #[test]
3216    fn ghost_count_bounded() {
3217        let mut storage: ParameterStorage = ParameterStorage::new();
3218        let mut server = ParameterServer::new_in(storage.as_table());
3219        for i in 0..MAX_PARAMETERS {
3220            let mut name = heapless::String::<64>::new();
3221            let _ = core::fmt::write(&mut name, format_args!("p{}", i));
3222            assert!(server.declare(NODE, name.as_str(), ParameterValue::Integer(i as i64)));
3223        }
3224        let ghost = ghost_from_server(&server);
3225        assert!(ghost.count <= ghost.max);
3226        assert_eq!(ghost.count, MAX_PARAMETERS);
3227        // Next declare should fail — count stays bounded
3228        assert!(!server.declare(NODE, "overflow", ParameterValue::Integer(0)));
3229        let ghost2 = ghost_from_server(&server);
3230        assert!(ghost2.count <= ghost2.max);
3231    }
3232}
3233
3234// =============================================================================
3235// Kani bounded model checking proofs
3236// =============================================================================
3237
3238#[cfg(kani)]
3239mod verification {
3240    use super::*;
3241
3242    /// phase-426 W1 — the node these single-node tests are about.
3243    const NODE: NodeKey = NodeKey::PRIMARY;
3244
3245    // phase-382 W2' — each proof used to build the whole store on the proof
3246    // stack (`ParameterServer::new()`); it now places a `ParameterStorage` and
3247    // lends it, which is the only shape a caller-owned store has. Kani still
3248    // sees the same `MAX_PARAMETERS` slots, so the bounds are unchanged.
3249
3250    #[kani::proof]
3251    fn server_new_is_empty() {
3252        let mut storage: ParameterStorage = ParameterStorage::new();
3253        let server = ParameterServer::new_in(storage.as_table());
3254        assert!(server.is_empty(NODE));
3255        assert_eq!(server.len(NODE), 0);
3256        assert!(!server.is_full());
3257    }
3258
3259    #[kani::proof]
3260    fn server_declare_get_roundtrip_integer() {
3261        let mut storage: ParameterStorage = ParameterStorage::new();
3262        let mut server = ParameterServer::new_in(storage.as_table());
3263        let val: i64 = kani::any();
3264        assert!(server.declare(NODE, "test", ParameterValue::Integer(val)));
3265        assert_eq!(server.get_integer(NODE, "test"), Some(val));
3266        assert!(server.has(NODE, "test"));
3267        assert_eq!(server.len(NODE), 1);
3268    }
3269
3270    #[kani::proof]
3271    fn server_declare_get_roundtrip_bool() {
3272        let mut storage: ParameterStorage = ParameterStorage::new();
3273        let mut server = ParameterServer::new_in(storage.as_table());
3274        let val: bool = kani::any();
3275        assert!(server.declare(NODE, "test", ParameterValue::Bool(val)));
3276        assert_eq!(server.get_bool(NODE, "test"), Some(val));
3277    }
3278
3279    #[kani::proof]
3280    fn server_set_requires_declare() {
3281        let mut storage: ParameterStorage = ParameterStorage::new();
3282        let mut server = ParameterServer::new_in(storage.as_table());
3283        let val: i64 = kani::any();
3284        // Set without declare should fail
3285        let result = server.set(NODE, "test", ParameterValue::Integer(val));
3286        assert_eq!(result, Err(ParameterError::NotFound));
3287    }
3288
3289    #[kani::proof]
3290    fn server_duplicate_declare_fails() {
3291        let mut storage: ParameterStorage = ParameterStorage::new();
3292        let mut server = ParameterServer::new_in(storage.as_table());
3293        assert!(server.declare(NODE, "test", ParameterValue::Integer(1)));
3294        assert!(!server.declare(NODE, "test", ParameterValue::Integer(2)));
3295        // Original value preserved
3296        assert_eq!(server.get_integer(NODE, "test"), Some(1));
3297    }
3298
3299    #[kani::proof]
3300    fn server_remove_clears() {
3301        let mut storage: ParameterStorage = ParameterStorage::new();
3302        let mut server = ParameterServer::new_in(storage.as_table());
3303        assert!(server.declare(NODE, "test", ParameterValue::Integer(42)));
3304        assert!(server.has(NODE, "test"));
3305        assert!(server.remove(NODE, "test"));
3306        assert!(!server.has(NODE, "test"));
3307        assert_eq!(server.len(NODE), 0);
3308    }
3309
3310    #[kani::proof]
3311    fn server_get_nonexistent_returns_none() {
3312        let mut storage: ParameterStorage = ParameterStorage::new();
3313        let server = ParameterServer::new_in(storage.as_table());
3314        assert!(server.get(NODE, "nonexistent").is_none());
3315        assert!(server.get_bool(NODE, "nonexistent").is_none());
3316        assert!(server.get_integer(NODE, "nonexistent").is_none());
3317        assert!(server.get_double(NODE, "nonexistent").is_none());
3318        assert!(server.get_string(NODE, "nonexistent").is_none());
3319    }
3320}