ridl_rt/contract.rs
1//! Identity, and the interaction descriptors generated code writes.
2//!
3//! A descriptor is a zero-sized marker type with constants. The descriptors
4//! are the per-member form of the ordinal table of ADR-0013 decision 3,
5//! extended with the interface number and the catalog hash.
6//!
7//! Two computations read the descriptors: a member's call deadline
8//! ([`Member::call_deadline`]) and the in-flight byte budget
9//! ([`Member::reservation`], [`table_budget`]).
10
11use crate::encoding::Encoding;
12use crate::sample::Duration;
13
14/// A member's ordinal: its position in the interface body, counted from 1
15/// (ridl §11). An ordinal is never 0.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct Ordinal(pub u32);
18
19/// An interface's number in its catalog: the number the lock file froze, or a
20/// provisional number. An interface number is never 0.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct InterfaceNo(pub u32);
23
24/// The catalog hash: SHA-256 over the catalog's interfaces, their numbers, and
25/// the types they reach.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct CatalogHash(pub [u8; 32]);
28
29/// A catalog: the interfaces of one unit — one package manifest and the
30/// source packages in its directory tree.
31///
32/// Two `CatalogRef`s are equal only when both the names and the hashes are
33/// equal, because two units with the same contents can have the same hash.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct CatalogRef {
36 /// The unit name.
37 pub name: &'static str,
38 /// The catalog hash.
39 pub hash: CatalogHash,
40}
41
42/// The five interaction kinds of the language.
43#[repr(u8)]
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub enum Kind {
46 /// A `signal`.
47 Signal = 1,
48 /// An `event`.
49 Event = 2,
50 /// A `command`.
51 Command = 3,
52 /// A `query`.
53 Query = 4,
54 /// A `fixed`.
55 Fixed = 5,
56}
57
58/// An interface's descriptor.
59pub trait Interface {
60 /// The catalog the interface belongs to.
61 const CATALOG: &'static CatalogRef;
62 /// The interface number in that catalog.
63 const NUMBER: InterfaceNo;
64 /// `true` when the number is provisional, not yet frozen in the lock file.
65 const PROVISIONAL: bool;
66 /// The interface name.
67 const NAME: &'static str;
68 /// The members, in ordinal order. A reserved ordinal has no row, so the
69 /// index of a row is not its ordinal minus one.
70 const MEMBERS: &'static [Member];
71}
72
73/// An interaction's descriptor: the part every kind has.
74pub trait Interaction {
75 /// The interface that declares the interaction.
76 type Iface: Interface;
77 /// The interaction's row in `Iface::MEMBERS`.
78 const MEMBER: &'static Member;
79}
80
81/// A `signal` (ridl §4).
82pub trait Signal: Interaction {
83 /// The value type.
84 type Payload;
85 /// The channel's init value (ridl §4.4).
86 fn init() -> Self::Payload;
87}
88
89/// An `event` (ridl §5).
90pub trait Event: Interaction {
91 /// The occurrence type.
92 type Payload;
93}
94
95/// A `fixed` (ridl §8).
96pub trait Fixed: Interaction {
97 /// The provisioned value type.
98 type Payload;
99}
100
101/// A `command` (ridl §6).
102pub trait Command: Interaction {
103 /// The argument type.
104 type Args;
105 /// Evaluates the command's `require` clauses. `Ok` when every clause is
106 /// true or the command declares none. `Err` when a clause is false, which
107 /// the provider reports as
108 /// [`Contract::PreconditionFailed`](crate::error::Contract::PreconditionFailed).
109 #[allow(
110 clippy::result_unit_err,
111 reason = "the failing method decides the contract error, so the error carries no value"
112 )]
113 fn require(args: &Self::Args) -> Result<(), ()>;
114}
115
116/// A `query` (ridl §7).
117pub trait Query: Interaction {
118 /// The argument type.
119 type Args;
120 /// The reply type.
121 type Reply;
122 /// Evaluates the query's `require` clauses. `Ok` when every clause is
123 /// true or the query declares none. `Err` when a clause is false, which
124 /// the provider reports as
125 /// [`Contract::PreconditionFailed`](crate::error::Contract::PreconditionFailed).
126 #[allow(
127 clippy::result_unit_err,
128 reason = "the failing method decides the contract error, so the error carries no value"
129 )]
130 fn require(args: &Self::Args) -> Result<(), ()>;
131 /// Evaluates the query's `ensure` clauses. `Ok` when every clause is true
132 /// or the query declares none. `Err` when a clause is false, which the
133 /// provider reports as
134 /// [`Contract::ContractBroken`](crate::error::Contract::ContractBroken).
135 #[allow(
136 clippy::result_unit_err,
137 reason = "the failing method decides the contract error, so the error carries no value"
138 )]
139 fn ensure(args: &Self::Args, reply: &Self::Reply) -> Result<(), ()>;
140}
141
142/// One member of an interface.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct Member {
145 /// The member's ordinal.
146 pub ordinal: Ordinal,
147 /// The member's kind.
148 pub kind: Kind,
149 /// The member's name.
150 pub name: &'static str,
151 /// The member's timing, as the IR resolved it. `None` when the IR carries
152 /// no timing: a `fixed`, or a `command` or `query` in a catalog built
153 /// before commands and queries took a default response bound.
154 pub timing: Option<Timing>,
155 /// One entry per payload: two for a `query` (the request, then the
156 /// reply), one for every other kind.
157 pub payloads: &'static [PayloadInfo],
158}
159
160impl Member {
161 /// The call deadline: the `max` of the member's `timing`, which on a
162 /// `command` or a `query` is the response bound (ridl §9.3).
163 ///
164 /// A `command` or a `query` in a catalog built by this version always has
165 /// a `max`: an untimed member takes the package's default response bound
166 /// (ridl §9.1). `None` remains for a catalog built before that default
167 /// existed, where such a member has no timing or a timing without `max`.
168 /// This function takes no position on what a caller does with `None`.
169 ///
170 /// The function reads `max` whatever the member's `kind`. On a `signal`
171 /// `max` is the staleness bound and on an `event` the time to live
172 /// (ridl §9), neither of which is a call deadline, so call it on a
173 /// `command` or a `query`.
174 pub fn call_deadline(&self) -> Option<Duration> {
175 self.timing.and_then(|timing| timing.max)
176 }
177
178 /// The bytes one in-flight instance of this member reserves in encoding
179 /// `E`: the sum of `PayloadInfo::max_size` for `E` over its `payloads` —
180 /// one payload for most kinds, two for a `query`, the request and then the
181 /// reply.
182 ///
183 /// No specification defines this budget. It is derived from the
184 /// descriptors alone, so that every runtime sizing a table of calls in
185 /// flight computes the same number.
186 ///
187 /// # Errors
188 ///
189 /// [`Unsized`], naming this member and the first payload whose size for
190 /// `E` is `None`. A missing size is reported, never replaced by an
191 /// estimate.
192 pub fn reservation<E: Encoding>(&self) -> Result<u64, Unsized> {
193 let mut total: u64 = 0;
194 for payload in self.payloads {
195 let size = E::max_size(&payload.max_size).ok_or(Unsized {
196 ordinal: self.ordinal,
197 member: self.name,
198 type_name: payload.type_name,
199 })?;
200 total = total.saturating_add(u64::from(size));
201 }
202 Ok(total)
203 }
204}
205
206/// The in-flight byte budget of a table of members in encoding `E`: the sum of
207/// [`Member::reservation`] over `members`. Pass an interface's
208/// `Interface::MEMBERS`; a table serving several interfaces adds the budget of
209/// each.
210///
211/// No specification defines this budget. It is derived from the descriptors
212/// alone, and it counts every member it is given, whatever its kind.
213///
214/// # Errors
215///
216/// [`Unsized`] for the first member, in the order given, whose reservation has
217/// a payload with no size for `E`.
218pub fn table_budget<E: Encoding>(members: &[Member]) -> Result<u64, Unsized> {
219 let mut total: u64 = 0;
220 for member in members {
221 total = total.saturating_add(member.reservation::<E>()?);
222 }
223 Ok(total)
224}
225
226/// A payload with no size in the encoding a budget was asked for: its
227/// `EncodedSizes` field for that encoding is `None`. The budget is not
228/// computed, because a missing size is not estimated.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230#[non_exhaustive]
231pub struct Unsized {
232 /// The member's ordinal.
233 pub ordinal: Ordinal,
234 /// The member's name.
235 pub member: &'static str,
236 /// The name of the payload type that has no size.
237 pub type_name: &'static str,
238}
239
240/// The form of a timing annotation (ridl §9).
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum TimingMode {
243 /// `@Xms`: a strict period, on a signal only (ridl §9.2).
244 StrictPeriodic,
245 /// `@[min..max]`, where either side may be absent.
246 Range,
247}
248
249/// A member's timing (ridl §9).
250///
251/// `max` is the staleness bound of a signal, the time to live of an event, and
252/// the response bound of a call.
253///
254/// Under `StrictPeriodic`, `min` and `max` both hold the period.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub struct Timing {
257 /// The form of the annotation.
258 pub mode: TimingMode,
259 /// The lower bound. `None` when the IR leaves it unset.
260 pub min: Option<Duration>,
261 /// The upper bound. `None` when the IR leaves it unset.
262 pub max: Option<Duration>,
263}
264
265/// One payload of a member.
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub struct PayloadInfo {
268 /// The payload type's name.
269 pub type_name: &'static str,
270 /// The payload's largest encoded size in each core encoding.
271 pub max_size: EncodedSizes,
272}
273
274/// A payload's largest encoded size in bytes, one field per core encoding.
275///
276/// A field is `None` when the toolchain cannot size the payload for that
277/// encoding. That covers two cases the reader does not have to tell apart: the
278/// encoding cannot carry the payload at all, and the encoding can carry it but
279/// the size is not derivable yet, because the layout is not defined yet
280/// (driftsys/ridl#317). The `repr(C)` column is `None` for every payload, and a backend that emits descriptors
281/// before its codec exists writes `None` for that codec's column too.
282///
283/// So a consumer reads `None` as "no size is available here", never as "this
284/// payload cannot be encoded this way", and asks the catalog descriptor rather
285/// than this field when it needs to know which encodings a payload has.
286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
287pub struct EncodedSizes {
288 /// The proto3 size.
289 pub proto3: Option<u32>,
290 /// The FlatBuffers size.
291 pub flatbuffers: Option<u32>,
292 /// The `repr(C)` size.
293 pub repr_c: Option<u32>,
294}
295
296#[cfg(test)]
297mod tests {
298 use super::{CatalogHash, CatalogRef, Kind};
299
300 #[test]
301 fn catalog_refs_are_equal_only_when_name_and_hash_are_equal() {
302 let base = CatalogRef {
303 name: "vehicle",
304 hash: CatalogHash([1; 32]),
305 };
306 let same = CatalogRef {
307 name: "vehicle",
308 hash: CatalogHash([1; 32]),
309 };
310 let other_hash = CatalogRef {
311 name: "vehicle",
312 hash: CatalogHash([2; 32]),
313 };
314 let other_name = CatalogRef {
315 name: "cabin",
316 hash: CatalogHash([1; 32]),
317 };
318 assert_eq!(base, same);
319 assert_ne!(base, other_hash);
320 assert_ne!(base, other_name);
321 }
322
323 #[test]
324 fn kind_values_are_the_language_order_from_one() {
325 assert_eq!(Kind::Signal as u8, 1);
326 assert_eq!(Kind::Event as u8, 2);
327 assert_eq!(Kind::Command as u8, 3);
328 assert_eq!(Kind::Query as u8, 4);
329 assert_eq!(Kind::Fixed as u8, 5);
330 }
331}