Skip to main content

nros_node/executor/
declared_shape.rs

1//! phase-457 W5 (issue 1522) — which registration entry point a DECLARED
2//! subscription reaches, as ONE value.
3//!
4//! # The hole this fills
5//!
6//! W3 made `[[endpoint]] registration_path`'s third row an OBSERVATION:
7//! `SubscriptionRequest::in_place_capable` is stated at each of the executor's
8//! eleven subscription entry points, read at
9//! [`Executor::open_subscription`][open], and reported from there through
10//! [`registration_observer`][obs]. A row nobody observed REFUSES, and every
11//! consumer then budgets the receive region — the safe direction.
12//!
13//! The **Rust producer observes nothing**. `nros::record_node_metadata::<C>`
14//! runs a component's `register()` against a recording `NodeContext` and opens
15//! no executor at all, so no registration happens in the probe and every Rust
16//! endpoint's row refuses. That is issue 1522.
17//!
18//! # Why a classifier is allowed here, when W3 deleted one
19//!
20//! W3 deleted an inference: the sizing descriptor composed the in-place row
21//! from the entry's LANGUAGE, which is wrong for nine of the eleven entry
22//! points. That was a SECOND OPINION — a table in the CLI about code it cannot
23//! see.
24//!
25//! The declarative road is not that shape. Its registrar is ONE function —
26//! `nros::node_runtime`'s `EntityKind::Subscription` arm — and it lowers every
27//! declared subscription to exactly one of two entry points. So the shape a
28//! declared endpoint takes is a FUNCTION OF THE DECLARATION, computed in one
29//! place, and this enum is that function's codomain.
30//!
31//! It stays one opinion only because of the two rules below, both gated by
32//! `check-declared-subscription-shape`:
33//!
34//! 1. **The registrar branches on it.** The subscription arm does not test the
35//!    declaration's `safety` flag itself; it asks for a
36//!    [`DeclaredSubscriptionShape`] and dispatches on the answer. A variant it
37//!    does not name is a gate failure, not a silent fall-through.
38//! 2. **The entry point takes its value from here.** Each entry point's
39//!    `SubscriptionRequest::in_place_capable` is spelled
40//!    `DeclaredSubscriptionShape::<V>.in_place_capable()`, so there is exactly
41//!    one `bool` per shape in the tree, and the entry point and the probe move
42//!    together when it changes — which is what makes this cheaper than
43//!    teaching the probe to register.
44//!
45//!    **Moving together is necessary, not sufficient.** Issue 1340 flipped
46//!    `BufferedRaw` to `true`, and this doc used to call that "a one-line
47//!    change here". It was not, and the one-line version is UNSAFE: the probe
48//!    would state `in_place`, the build would price the row at no receive
49//!    region, and the executor — which then ignored `SubscriptionOpen::in_place`
50//!    on this path — would still build a buffered entry and claim the full
51//!    region. That is the under-size direction, `BufferTooSmall` at boot. The
52//!    flip is safe only because `register_subscription_buffered_raw_on` now
53//!    BRANCHES on the report; a `true` here is a promise that the entry point
54//!    acts on it. `a_generic_subscription_on_an_in_place_backend_claims_no_
55//!    receive_region` fails first on a flip without the branch.
56//!
57//! [open]: super::Executor
58//! [obs]: super::registration_observer
59
60/// Which of the executor's registration entry points a DECLARED subscription
61/// lowers to.
62///
63/// One variant per lowering the declarative registrar can choose. Exhaustive:
64/// a new lowering is a new variant, which breaks
65/// [`in_place_capable`](Self::in_place_capable) at compile time and the
66/// registrar at its gate.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum DeclaredSubscriptionShape {
69    /// `Node::create_generic_subscription_with_qos` →
70    /// `Executor::register_subscription_buffered_raw_on`.
71    BufferedRaw,
72    /// A `.safety()` declaration:
73    /// `Node::create_generic_subscription_with_integrity` →
74    /// `Executor::register_subscription_buffered_raw_safety_on`.
75    BufferedRawSafety,
76}
77
78impl DeclaredSubscriptionShape {
79    /// Classify a declaration.
80    ///
81    /// `safety` is the declaration's `.safety()` flag **as the runtime will
82    /// see it**: a build whose `safety-e2e` capability is off ignores the flag
83    /// and registers the basic path, so its caller masks the flag to `false`
84    /// before asking. That mask has exactly one home on the caller's side, for
85    /// the same reason this classifier has one here.
86    pub const fn of_declaration(safety: bool) -> Self {
87        if safety {
88            Self::BufferedRawSafety
89        } else {
90            Self::BufferedRaw
91        }
92    }
93
94    /// Can a registration of this shape dispatch out of the backend's own
95    /// receive slot?
96    ///
97    /// **The definition, not a copy of one.** Each entry point writes this
98    /// call into its `SubscriptionRequest`, so the answer a probe states and
99    /// the answer the executor acts on are the same expression.
100    ///
101    /// The two shapes differ, and the reason each has its answer is worth a
102    /// sentence:
103    ///
104    /// * **`BufferedRaw` — `true`** (issue 1340). Its callback is
105    ///   `FnMut(&[u8])`, borrowed bytes and nothing else, which is exactly what
106    ///   `process_raw_in_place` hands over. It was `false` while its arena entry
107    ///   was believed shared with `add_arena_subscription_callback`; that caller
108    ///   never reaches `open_subscription`, so the in-place arm could be local to
109    ///   the one entry point. A true here is a claim the build PRICES: the
110    ///   probe states `in_place` for every declared non-safety subscription, and
111    ///   on an in-place backend that endpoint reserves no receive region.
112    /// * **`BufferedRawSafety` — `false`.** `process_raw_in_place` has no
113    ///   validating form: an integrity-checked sample must be verified before
114    ///   the callback sees it, and the borrowed slot offers nowhere to put the
115    ///   verdict. Not a limit anyone plans to lift by flipping this.
116    pub const fn in_place_capable(self) -> bool {
117        match self {
118            Self::BufferedRaw => true,
119            Self::BufferedRawSafety => false,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn a_declaration_classifies_by_its_safety_flag() {
130        assert_eq!(
131            DeclaredSubscriptionShape::of_declaration(false),
132            DeclaredSubscriptionShape::BufferedRaw
133        );
134        assert_eq!(
135            DeclaredSubscriptionShape::of_declaration(true),
136            DeclaredSubscriptionShape::BufferedRawSafety
137        );
138    }
139
140    /// Not a restatement of `in_place_capable`'s body: this is the claim the
141    /// PROBE ships, so it is what every Rust image's declared subscriptions are
142    /// PRICED at.
143    ///
144    /// It was `neither_declarative_shape_dispatches_in_place_today` until issue
145    /// 1340 flipped the raw arm, and that rename is the point: this test is
146    /// where a change of price is said out loud. A declared non-safety
147    /// subscription now states `in_place`, so on zenoh and XRCE it claims no
148    /// receive region; a `.safety()` one still buffers.
149    #[test]
150    fn only_the_non_safety_declarative_shape_dispatches_in_place() {
151        assert!(DeclaredSubscriptionShape::BufferedRaw.in_place_capable());
152        assert!(!DeclaredSubscriptionShape::BufferedRawSafety.in_place_capable());
153    }
154}