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}