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