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