Skip to main content

thruput_sdk/exn/
fault.rs

1//! What a contract fault carries: [`FaultString`], the bounded message the
2//! exception types hold, and [`FaultInfo`], the panic record behind
3//! [`RawCallError::Fault`](crate::call::RawCallError::Fault).
4//!
5//! `FaultString` encodes to **zero bytes on every target and every feature
6//! combination**. The message it carries is host-side diagnostic state, not
7//! wire data: it is written where the fault is raised and read by the native
8//! test harness, and it never reaches a serializer. That is deliberate —
9//! `FaultString` appears inside [`LibraryExn`](crate::exn::LibraryExn), whose
10//! encoding must not vary with how the crate was built, or a contract's abort
11//! payload would differ between a host test and the artifact that ships.
12//!
13//! On `riscv32` the message is absent entirely: the field, the formatting, and
14//! the buffer all compile out, so a guest pays nothing for it.
15//!
16//! [`FaultInfo`] is a separate story: it is not a wire type and holds the
17//! harness's record verbatim, so it exists only under `dev` and is
18//! zero-sized everywhere else.
19
20use crate::xdr::*;
21use core::fmt::{self, Debug};
22
23// Host-only, like everything that reads or renders a fault's message.
24#[cfg(not(target_arch = "riscv32"))]
25use core::error::Error;
26#[cfg(not(target_arch = "riscv32"))]
27use core::fmt::Display;
28
29/// The harness's own types, held directly rather than mirrored — see
30/// [`FaultInfo`].
31#[cfg(feature = "dev")]
32use thruput_native_exec::invoke_error::{PanicLocation, PanicRecord};
33
34/// Capacity of the host-side message. Host-only: a guest has no field to size.
35#[cfg(not(target_arch = "riscv32"))]
36const CAP: usize = 500;
37
38/// What a fault with no recorded message reads as. Shared by [`FaultString`]
39/// and [`FaultInfo`], which are asserted against it across two crates' tests.
40#[cfg(not(target_arch = "riscv32"))]
41const NO_MESSAGE: &str = "<fault>";
42
43/// A fault's diagnostic message.
44///
45/// Carries a human-readable description of a fault on the host and nothing at
46/// all on a guest. Encodes to zero bytes regardless — see the module docs.
47/// Two `FaultString`s are always equal: the message is diagnostic, so it is
48/// not part of the value's identity, and comparisons behave the same on host
49/// and guest.
50#[derive(Clone, Default)]
51pub struct FaultString {
52    #[cfg(not(target_arch = "riscv32"))]
53    msg: Option<heapless::String<CAP>>,
54}
55
56impl FaultString {
57    /// The message, or `"<fault>"` when none was recorded.
58    ///
59    /// Host-only: a guest records no message, so there is nothing for this
60    /// to return and it does not exist there. Reading a fault's text is a
61    /// diagnostic act, and diagnostics are a host concern.
62    #[cfg(not(target_arch = "riscv32"))]
63    pub fn as_str(&self) -> &str {
64        self.msg.as_ref().map(|s| s.as_str()).unwrap_or(NO_MESSAGE)
65    }
66
67    /// A fault with no message.
68    #[inline]
69    pub fn empty() -> Self {
70        Self::default()
71    }
72
73    /// Record `v`'s `Display` form as the message. A message longer than `CAP`
74    /// is replaced with `<too-big>`.
75    #[cfg(not(target_arch = "riscv32"))]
76    pub fn via_display<V>(v: &V) -> Self
77    where
78        V: Display + ?Sized,
79    {
80        use core::fmt::Write;
81
82        let mut s = heapless::String::new();
83        write!(s, "{}", v).unwrap_or_else(|_| {
84            s.clear();
85            let _ = write!(s, "<too-big>");
86        });
87
88        Self { msg: Some(s) }
89    }
90
91    /// Discard `v`: a guest records no message.
92    ///
93    /// Deliberately unbounded rather than `V: Display`. Several callers pass
94    /// an error whose `Display` is itself only compiled in some
95    /// configurations, so requiring the bound here would fail the guest
96    /// build for types that never get formatted on a guest anyway.
97    #[cfg(target_arch = "riscv32")]
98    pub fn via_display<V: ?Sized>(_v: &V) -> Self {
99        Self {}
100    }
101
102    /// [`via_display`](Self::via_display) for a value passed by value.
103    #[cfg(not(target_arch = "riscv32"))]
104    pub fn from_display(s: impl Display) -> Self {
105        Self::via_display(&s)
106    }
107
108    /// [`via_display`](Self::via_display) for a value passed by value.
109    #[cfg(target_arch = "riscv32")]
110    pub fn from_display<S>(_s: S) -> Self {
111        Self {}
112    }
113}
114
115// Hand-written rather than derived: the message is host-only state, so
116// deriving would make equality (and `Debug`'s shape) differ by target.
117impl PartialEq for FaultString {
118    fn eq(&self, _other: &Self) -> bool {
119        true
120    }
121}
122
123impl Eq for FaultString {}
124
125// `Debug` is the one formatting impl a guest needs: `LibraryExn` derives it,
126// and `contract_abort_encoded` takes `T: Debug`. The guest arm prints no
127// message because it has none to print.
128#[cfg(not(target_arch = "riscv32"))]
129impl Debug for FaultString {
130    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131        f.debug_tuple("FaultString").field(&self.as_str()).finish()
132    }
133}
134
135// Writes nothing rather than the type's name: a guest has no message, and a
136// name would be a string literal in every contract that instantiates this
137// impl, for a value that is always empty there. The impl exists only because
138// the bound demands it -- `LibraryExn` derives `Debug` and the throw helpers
139// take `T: Debug` -- not because a guest has anything to print.
140#[cfg(target_arch = "riscv32")]
141impl Debug for FaultString {
142    fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
143        Ok(())
144    }
145}
146
147#[cfg(not(target_arch = "riscv32"))]
148impl Error for FaultString {}
149
150#[cfg(not(target_arch = "riscv32"))]
151impl Display for FaultString {
152    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153        f.write_str(self.as_str())
154    }
155}
156
157// A guest has no message, so this renders nothing -- deliberately not even a
158// placeholder, which would put a string in every contract for a value that is
159// always empty there. It exists at all because generated client code can
160// format a `FaultString` with `{}`.
161#[cfg(target_arch = "riscv32")]
162impl core::fmt::Display for FaultString {
163    fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
164        Ok(())
165    }
166}
167
168// Wire form: empty, on every target. Hand-written because the struct has a
169// target-gated field that must never reach the encoding -- see module docs.
170impl WriteXDR for FaultString {
171    fn write_xdr<F, W: xdrs::Writer<F>>(
172        &self,
173        _w: &mut W,
174    ) -> xdrs::WriteResult<()> {
175        Ok(())
176    }
177
178    unsafe fn write_xdr_unchecked<F, W: xdrs::WriterUnchecked<F>>(
179        &self,
180        _w: &mut W,
181    ) {
182    }
183}
184
185impl<'r> ReadXDR<'r> for FaultString {
186    fn read_xdr<R: xdrs::Reader<'r>>(_r: &mut R) -> xdrs::ReadResult<Self> {
187        Ok(Self::empty())
188    }
189}
190
191impl xdrs::cxs::ConstMaxSize for FaultString {
192    const MAX_SIZE: Option<usize> = Some(0);
193}
194
195impl xdrs::txs::ConstMaxSize for FaultString {
196    const MAX_SIZE: Option<usize> = Some(0);
197}
198
199// A guest carries no message: no field, no buffer, no formatting. Pinned
200// here so a `cfg` slip that let the host-side field through would fail the
201// guest build rather than quietly costing every contract 500-odd bytes of
202// stack per live fault value.
203#[cfg(target_arch = "riscv32")]
204const _: () = assert!(core::mem::size_of::<FaultString>() == 0);
205
206// The property the rest of the exception machinery depends on: a
207// `FaultString` contributes nothing to any enclosing type's size bound, so
208// `LibraryExn`'s wire size (and `ABORT_MAX`) cannot vary with the message or
209// with how the crate was built.
210const _: () = assert!(matches!(
211    <FaultString as xdrs::cxs::ConstMaxSize>::MAX_SIZE,
212    Some(0)
213));
214
215impl crate::__contract_spec::ContractTypeName for FaultString {
216    const NAME: crate::__contract_spec::ContractSpecIdView<'static> =
217        crate::__contract_spec::ContractSpecIdView {
218            crate_name: ::core::env!("CARGO_PKG_NAME"),
219            major: crate::exn::EXN_ABI_MAJOR,
220            module_path: ::core::module_path!(),
221            declared: "FaultString",
222        };
223}
224
225impl crate::__contract_spec::ContractTypeSpec for FaultString {
226    const TYPE_DEF: crate::__contract_spec::ContractTypeDefView<'static> =
227        crate::__contract_spec::ContractTypeDefView::Struct(
228            crate::__contract_spec::ContractStructDefView {
229                name: <Self as crate::__contract_spec::ContractTypeName>::NAME,
230                fields: &[],
231            },
232        );
233}
234
235impl crate::__contract_spec::ContractTypeRefSpec for FaultString {
236    const INFO: crate::__contract_spec::ContractTypeInfo<'static> =
237        crate::__contract_spec::ContractTypeInfo {
238            ty: crate::__contract_spec::ContractTypeView::Named(
239                <Self as crate::__contract_spec::ContractTypeName>::NAME,
240            ),
241        };
242
243    const __THRUPUT_SPEC_ANCHOR_ROOT: fn() -> usize =
244        Self::__thruput_spec_anchor;
245
246    #[cfg_attr(target_arch = "riscv32", inline(never))]
247    #[cfg_attr(
248        target_arch = "riscv32",
249        unsafe(link_section = "thruputspecanchortextv0")
250    )]
251    fn __thruput_spec_anchor() -> usize {
252        0
253    }
254}
255
256#[doc(hidden)]
257#[allow(non_upper_case_globals)]
258pub const __thruput_spec_fragment_FaultString:
259    crate::__contract_spec::SpecFragmentView<'static> =
260    crate::__contract_spec::SpecFragmentView::TypeDef(
261        <FaultString as crate::__contract_spec::ContractTypeSpec>::TYPE_DEF,
262    );
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    /// The wire form is empty whatever the message -- this is the property
269    /// that keeps a contract's abort payload identical across build configs.
270    #[test]
271    fn encodes_to_nothing_whatever_the_message() {
272        for fs in [
273            FaultString::empty(),
274            FaultString::from_display("boom"),
275            FaultString::from_display(
276                core::str::from_utf8(&[b'x'; 500]).unwrap(),
277            ),
278        ] {
279            assert!(txs::encode(&fs).unwrap().is_empty());
280        }
281    }
282
283    #[test]
284    fn decodes_from_nothing() {
285        let s: FaultString = txs::decode(&[]).unwrap();
286        assert_eq!(s.as_str(), "<fault>");
287    }
288
289    /// The message is readable in-process, which is the only place it is
290    /// ever meant to be read.
291    #[test]
292    fn message_is_readable_host_side() {
293        assert_eq!(FaultString::from_display("boom").as_str(), "boom");
294        assert_eq!(FaultString::empty().as_str(), "<fault>");
295    }
296
297    /// An oversized message degrades rather than panicking or truncating
298    /// into something misleading.
299    #[test]
300    fn oversized_message_is_marked() {
301        let long = core::str::from_utf8(&[b'x'; CAP + 1]).unwrap();
302        assert_eq!(FaultString::from_display(long).as_str(), "<too-big>");
303    }
304}
305
306/// What the harness recorded about the panic that faulted a frame.
307///
308/// Wraps the harness's `PanicRecord` so that
309/// [`RawCallError::Fault`](crate::call::RawCallError::Fault) has a payload
310/// type on every target: that record exists only under `dev`, and an enum
311/// variant's field cannot carry a `cfg` of its own. Without `dev` this is
312/// zero-sized, so a contract pays nothing for carrying one.
313///
314/// Two `FaultInfo`s are always equal, following [`FaultString`]: the contents
315/// are diagnostic, not part of the value's identity.
316#[cfg_attr(not(target_arch = "riscv32"), derive(Debug))]
317#[derive(Clone, Default)]
318pub struct FaultInfo {
319    #[cfg(feature = "dev")]
320    record: PanicRecord,
321}
322
323impl FaultInfo {
324    /// A fault with nothing recorded.
325    #[inline]
326    pub fn empty() -> Self {
327        Self::default()
328    }
329
330    /// The message, or `"<fault>"` when none was recorded.
331    #[cfg(not(target_arch = "riscv32"))]
332    pub fn as_str(&self) -> &str {
333        #[cfg(feature = "dev")]
334        if let Some(message) = self.record.message.as_deref() {
335            return message;
336        }
337        NO_MESSAGE
338    }
339
340    /// Where the panic happened, when the hook recorded a location.
341    #[cfg(feature = "dev")]
342    pub fn location(&self) -> Option<&PanicLocation> {
343        self.record.location.as_ref()
344    }
345}
346
347// Hand-written for the same reason as `FaultString`'s: the contents are
348// host-only state, so deriving would make equality differ by target.
349impl PartialEq for FaultInfo {
350    fn eq(&self, _other: &Self) -> bool {
351        true
352    }
353}
354
355impl Eq for FaultInfo {}
356
357/// Renders as `message at file:line:column`, dropping either half that was
358/// not recorded. With neither, this is `"<fault>"`.
359#[cfg(not(target_arch = "riscv32"))]
360impl Display for FaultInfo {
361    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
362        f.write_str(self.as_str())?;
363        #[cfg(feature = "dev")]
364        if let Some(location) = self.location() {
365            write!(f, " at {location}")?;
366        }
367        Ok(())
368    }
369}
370
371// Writes nothing, for the reason `FaultString`'s guest `Debug` does; the host
372// arm is the derive above.
373#[cfg(target_arch = "riscv32")]
374impl Debug for FaultInfo {
375    fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
376        Ok(())
377    }
378}
379
380#[cfg(not(target_arch = "riscv32"))]
381impl Error for FaultInfo {}
382
383#[cfg(feature = "dev")]
384impl From<&PanicRecord> for FaultInfo {
385    fn from(record: &PanicRecord) -> Self {
386        Self {
387            record: record.clone(),
388        }
389    }
390}
391
392// Without `dev` there is no record to hold, so the struct vanishes -- and a
393// guest can never enable `dev`, since `lib.rs` rejects `std` on rv32. Pinned
394// so a `cfg` slip would fail the build rather than quietly cost a contract
395// stack per live fault.
396#[cfg(not(feature = "dev"))]
397const _: () = assert!(core::mem::size_of::<FaultInfo>() == 0);