Skip to main content

thruput_sdk/exn/
match_exn.rs

1//! `match_exn!` and the [`MatchExn`] trait behind it.
2//!
3//! A Thruput exception is self-describing on the wire —
4//! `[CXS(selector)][payload]` — and the selector identifies the type
5//! independently of any interface's `throws` list. That is what lets
6//! [`match_exn!`](crate::match_exn) match an error declared by a base
7//! interface and one defined by an extension side by side, as peers.
8//!
9//! The user-facing rules live on the macro itself; this module holds the
10//! trait that decides what it can match on, and the decode helper its
11//! expansion calls.
12
13use super::{Exn, RawException, RawExceptionRef};
14use crate::call::RawCallError;
15use crate::xdr::ReadXDROwned;
16
17/// What [`match_exn!`](crate::match_exn) can match on.
18///
19/// Implemented for [`CallError`](crate::call::CallError) (the error type of
20/// ordinary generated `try_*` methods), for an owned [`RawException`], and for
21/// a borrowed [`RawExceptionRef`] — the last of which lets a caller match
22/// inside [`try_call_raw_with`](crate::call::try_call_raw_with)'s `on_exn`
23/// closure without first copying the wire bytes into an owned exception.
24/// Decoding matched types with owned fields can still copy or allocate.
25///
26/// Returning a reborrow by value, rather than a reference, is what keeps
27/// all three impls trivial.
28pub trait MatchExn {
29    /// The caught exception, or `None` if this failure carries none (a
30    /// fault, an invalid call, an encode/decode failure).
31    fn as_exn(&self) -> Option<RawExceptionRef<'_>>;
32
33    /// Decode as `E` if this failure carries an exception of that type.
34    ///
35    /// The single-type shorthand for [`match_exn!`](crate::match_exn) — no
36    /// `Unknown` detour, whatever the called interface declared it
37    /// `throws`:
38    ///
39    /// ```ignore
40    /// assert_eq!(
41    ///     token.try_transfer(&to, 1).unwrap_err().try_as(),
42    ///     Some(ERC20PausableError::EnforcedPause),
43    /// );
44    /// ```
45    ///
46    /// `None` covers all three ways this can miss: no exception at all, a
47    /// different exception type, or an `E`-tagged payload that fails to
48    /// decode. Use [`try_decode`](Self::try_decode) to separate the last of
49    /// those from the other two.
50    fn try_as<E: Exn + ReadXDROwned>(&self) -> Option<E> {
51        self.as_exn().and_then(|e| e.try_as())
52    }
53
54    /// [`try_as`](Self::try_as) with the decode failure kept separate:
55    ///
56    /// - `None` — this failure carries no exception, or one belonging to
57    ///   some other type. (Match on
58    ///   [`CallError::Exception`](crate::call::RawCallError::Exception) to
59    ///   tell those two apart.)
60    /// - `Some(Ok(e))` — the selector matched and the payload decoded.
61    /// - `Some(Err(_))` — the selector matched, but the payload does not
62    ///   decode as `E`.
63    ///
64    /// See [`RawExceptionRef::try_decode`] for when the third case arises;
65    /// it is not only forgery. Nothing in the SDK acts on it for you —
66    /// `match_exn!` treats it as a non-match, so a caller that must not
67    /// confuse "corrupt `E`" with "not an `E`" has to ask here.
68    fn try_decode<E: Exn + ReadXDROwned>(
69        &self,
70    ) -> Option<Result<E, crate::xdr::ReadError>> {
71        self.as_exn().and_then(|e| e.try_decode())
72    }
73}
74
75impl MatchExn for RawException {
76    fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
77        Some(self.as_ref_exn())
78    }
79}
80
81impl MatchExn for RawExceptionRef<'_> {
82    fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
83        // Covariance in the payload lifetime makes the copy usable here.
84        Some(*self)
85    }
86}
87
88impl<X: MatchExn> MatchExn for RawCallError<X> {
89    fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
90        match self {
91            RawCallError::Exception(x) => x.as_exn(),
92            _ => None,
93        }
94    }
95}
96
97/// A reference matches as its referent, so `match_exn!(&e, { .. })` and
98/// `match_exn!(e, { .. })` behave alike.
99impl<T: MatchExn + ?Sized> MatchExn for &T {
100    fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
101        (**self).as_exn()
102    }
103}
104
105/// One arm's decode attempt. Not public API: `match_exn!` emits calls to it.
106///
107/// `T` is left entirely to inference — the arm's pattern supplies it.
108#[doc(hidden)]
109pub fn __try_as<T: Exn + ReadXDROwned>(
110    exn: Option<RawExceptionRef<'_>>,
111) -> Option<T> {
112    exn.and_then(|e| e.try_as())
113}
114
115/// Match a caught exception against any exception type, whatever the called
116/// interface declared it `throws`.
117///
118/// ```ignore
119/// match token.try_transfer(&to, amount) {
120///     Ok(()) => { /* ... */ }
121///     Err(CallError::Fault(f)) => { /* ... */ }
122///     Err(e) => match_exn!(e, {
123///         ERC20Error::TooLarge               => { /* ... */ }
124///         ERC20PausableError::EnforcedPause  => { /* ... */ }
125///         LibraryExn::NonNegativeIntOverflow => { /* ... */ }
126///         _ => e.re_throw(),
127///     }),
128/// }
129/// ```
130///
131/// Everything structural — `Ok`, [`Fault`](crate::call::RawCallError::Fault),
132/// [`Invalid`](crate::call::RawCallError::Invalid),
133/// [`Reentrance`](crate::call::RawCallError::Reentrance), the rest of the
134/// pre-call rejections, the encode/decode
135/// failures — stays an ordinary Rust `match`, with real exhaustiveness
136/// checking. This macro's only job is typed downcasting. It accepts anything
137/// implementing [`MatchExn`](crate::exn::MatchExn): a
138/// [`CallError`](crate::call::CallError), an owned
139/// [`RawException`](crate::exn::RawException), or a borrowed
140/// [`RawExceptionRef`](crate::exn::RawExceptionRef).
141///
142/// # How arms are matched
143///
144/// Arms are tried **in order, first match wins** — they lower to a chain of
145/// `if let Some(<pattern>) = raw.try_as()`, not to a `match`. Each attempt is
146/// guarded by a selector comparison, so an arm naming an unrelated type costs
147/// one integer compare.
148///
149/// The macro never names the exception type: **the arm's pattern pins `T` in
150/// [`try_as::<T>`](crate::exn::RawExceptionRef::try_as) by ordinary type
151/// inference**, because a path pattern like `ERC20Error::TooLarge` unifies the
152/// scrutinee's type with `ERC20Error`. Two constraints follow from that, and
153/// they are mechanical rather than stylistic:
154///
155/// - **Every typed arm must be path-rooted** — `Type::Variant`,
156///   `Type::Variant { .. }`, or a unit-struct `Type`. A bare identifier is a
157///   *binding*, which leaves `T` unconstrained and fails inference with
158///   "type annotations needed". The untyped escape hatch is `_`, where the
159///   scrutinee itself is still in scope.
160/// - **`|` cannot cross types.** `ERC20Error::A | ERC20PausableError::B`
161///   would be one `try_as` with two `T`s. Use two arms. Alternates *within*
162///   one type are fine.
163///
164/// Guards (`Pat if cond =>`) work, and a failing guard falls through to the
165/// later arms rather than straight to `_`.
166///
167/// Commas follow `match`: an expression body needs a trailing comma, a block
168/// body may take one or leave it off.
169///
170/// # The `_` arm is required
171///
172/// Arms are sequential rather than exhaustiveness-checked, so nothing can
173/// prove the typed arms cover every case — and an exception whose selector
174/// matches no arm is the ordinary case, not an edge one. Requiring `_` keeps
175/// that control flow visible at the call site. A typical use is to propagate
176/// the error with `_ => e.re_throw()`.  (That arguably could be the invisible
177/// default, but isn't for the sake of clarity.)
178///
179/// # What reaches the `_` arm
180///
181/// The `_` arm is not only "some error type I did not name". Five distinct
182/// situations are swept into it, and the macro does not distinguish them:
183///
184/// 1. **No exception at all.** When the scrutinee is a
185///    [`CallError`](crate::call::CallError), every variant other than
186///    `Exception` — a fault, an invalid call, an argument-encoding or
187///    result-decoding failure — carries nothing to decode. These are worth
188///    handling as ordinary `match` arms on `CallError` *before* delegating
189///    to `match_exn!`, precisely so they do not land here.
190/// 2. **An exception of a type no arm names.** The expected case: an error
191///    the caller does not know about, or knows about and chose not to
192///    handle.
193/// 3. **An exception whose selector matches a named type, but whose payload
194///    does not decode as it.** A selector is
195///    `sha256(major_version :: crate :: type)` and says nothing about
196///    payload layout, so this is *not* only forgery or truncation: a callee
197///    that adds a variant to an exception enum makes a semver-compatible
198///    change that leaves the selector untouched, and an older caller cannot
199///    decode the result. It arrives here looking exactly like case 2.
200///    [`MatchExn::try_decode`] is the way to tell the two apart.
201/// 4. **A decoded exception that matched no arm's pattern.** `A::Y` when
202///    only `A::X` has an arm.
203/// 5. **A decoded exception whose every candidate arm had a failing
204///    guard.** Guards fall through to later arms, and then to here.
205///
206/// Case 3 is the one to keep in mind, because it is silent and can be
207/// reached without anyone doing anything wrong. If confusing "a corrupt `E`"
208/// with "not an `E`" would change a decision your contract makes, ask
209/// [`try_decode`](MatchExn::try_decode) rather than relying on the arms.
210///
211/// One near-miss does *not* reach `_`, for symmetry's sake: decoding reads a
212/// **prefix**, so a payload with extra trailing bytes decodes as the shorter
213/// shape and matches. A callee that appends a field to a struct exception is
214/// accepted silently by an older caller, with the added field dropped.
215///
216/// # Matching a single type
217///
218/// [`MatchExn::try_as`](crate::exn::MatchExn::try_as) is the shorthand when
219/// only one type is of interest:
220///
221/// ```ignore
222/// assert_eq!(
223///     token.try_transfer(&to, 1).unwrap_err().try_as(),
224///     Some(ERC20PausableError::EnforcedPause),
225/// );
226/// ```
227#[macro_export]
228macro_rules! match_exn {
229    ($e:expr, { $($arms:tt)* }) => {{
230        // The labeled block is a `let` initializer purely so a *statement*
231        // attribute can cover every `break` inside it (attributes on
232        // expressions are still unstable). Two expected-and-harmless
233        // conditions need silencing: an arm body that diverges (`panic!`,
234        // `re_throw()`) leaves the `break` carrying its value unreachable
235        // and makes it a diverging sub-expression, and a `match_exn!` whose
236        // only arm is `_` never breaks at all. `_ => e.re_throw()` is the
237        // documented way to propagate, so this must not warn in user
238        // crates building with `-D warnings`.
239        #[allow(
240            unreachable_code,
241            unused_labels,
242            clippy::diverging_sub_expression
243        )]
244        let __match_exn_result = '__match_exn: {
245            // `&$e`, not `$e`: a `let` extends the lifetime of a temporary
246            // scrutinee, and a named one is merely borrowed — leaving it
247            // usable inside the `_` arm.
248            let __match_exn_scrutinee = &$e;
249            let __match_exn_raw =
250                $crate::exn::MatchExn::as_exn(__match_exn_scrutinee);
251            $crate::__match_exn_arms!(
252                '__match_exn, __match_exn_raw, $($arms)*
253            )
254        };
255        __match_exn_result
256    }};
257}
258
259/// The arm muncher behind [`match_exn!`](crate::match_exn). Not public API.
260///
261/// Rule order is important: the `_` rules must precede the `$pat:pat`
262/// rules, since `_` is itself a valid pattern and would otherwise be
263/// lowered to a `try_as` with nothing to infer `T` from.
264#[doc(hidden)]
265#[macro_export]
266macro_rules! __match_exn_arms {
267    // -- terminal `_` arm ------------------------------------------------
268    ($lbl:lifetime, $exn:ident, _ => $body:block $(,)?) => {
269        break $lbl ($body)
270    };
271    ($lbl:lifetime, $exn:ident, _ => $body:expr $(,)?) => {
272        break $lbl ({ $body })
273    };
274    // A `_` arm with anything after it: later arms would be unreachable.
275    ($lbl:lifetime, $exn:ident, _ => $($rest:tt)*) => {
276        ::core::compile_error!(
277            "the `_` arm of `match_exn!` must come last; arms after it \
278             could never be reached"
279        )
280    };
281
282    // -- guarded typed arm ----------------------------------------------
283    //
284    // A block body takes an optional comma, so it needs two rules. NOT one
285    // rule with `$(,)? $($rest:tt)*`: when a comma is present the matcher
286    // cannot tell whether it belongs to the optional repetition or is the
287    // first `tt` of `$rest`, and macro_rules rejects that as a local
288    // ambiguity rather than choosing — a hard error at every call site
289    // written `Pat => { .. },`. Each rule below is a fixed token sequence,
290    // so neither is ambiguous. See `block_body_then_comma_*` in the tests.
291    ($lbl:lifetime, $exn:ident,
292     $pat:pat if $guard:expr => $body:block , $($rest:tt)*) => {{
293        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
294            && $guard
295        {
296            break $lbl ($body);
297        }
298        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
299    }};
300    ($lbl:lifetime, $exn:ident,
301     $pat:pat if $guard:expr => $body:block $($rest:tt)*) => {{
302        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
303            && $guard
304        {
305            break $lbl ($body);
306        }
307        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
308    }};
309    ($lbl:lifetime, $exn:ident,
310     $pat:pat if $guard:expr => $body:expr , $($rest:tt)*) => {{
311        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
312            && $guard
313        {
314            break $lbl ({ $body });
315        }
316        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
317    }};
318
319    // -- plain typed arm -------------------------------------------------
320    // Two rules for the optional comma, for the reason given above.
321    ($lbl:lifetime, $exn:ident, $pat:pat => $body:block , $($rest:tt)*) => {{
322        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
323        {
324            break $lbl ($body);
325        }
326        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
327    }};
328    ($lbl:lifetime, $exn:ident, $pat:pat => $body:block $($rest:tt)*) => {{
329        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
330        {
331            break $lbl ($body);
332        }
333        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
334    }};
335    ($lbl:lifetime, $exn:ident, $pat:pat => $body:expr , $($rest:tt)*) => {{
336        if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
337        {
338            break $lbl ({ $body });
339        }
340        $crate::__match_exn_arms!($lbl, $exn, $($rest)*)
341    }};
342
343    // -- ran out of arms without a `_` -----------------------------------
344    ($lbl:lifetime, $exn:ident $(,)?) => {
345        ::core::compile_error!(
346            "`match_exn!` requires a final `_ => ...` arm: its arms are \
347             tried in order rather than checked for exhaustiveness, so an \
348             exception matching none of them must be handled explicitly \
349             (`_ => e.re_throw()` propagates it)"
350        )
351    };
352}
353
354#[cfg(all(test, feature = "alloc"))]
355mod tests {
356    use super::*;
357    use crate::exn::{FaultInfo, LibraryExn};
358    use crate::internal::encode_xdr;
359    use crate::xdr::*;
360
361    // Hand-written `Exn` impls rather than `#[exn]`: these types exist only
362    // to exercise selector dispatch, and never reach a contract spec.
363    #[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
364    #[repr(i32)]
365    enum Alpha {
366        NotFound = 1,
367        Big { n: u64 } = 2,
368    }
369
370    impl Exn for Alpha {
371        const SELECTOR: u64 = 0xA1FA_0000_0000_0001;
372    }
373
374    #[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
375    #[repr(i32)]
376    enum Beta {
377        Rejected = 1,
378    }
379
380    impl Exn for Beta {
381        const SELECTOR: u64 = 0xBE7A_0000_0000_0001;
382    }
383
384    /// Build the owned exception a caller would catch for `e`.
385    fn caught<E: Exn + WriteXDR>(e: E) -> RawException {
386        RawException::new(E::SELECTOR, &encode_xdr(&e).unwrap())
387    }
388
389    fn err<E: Exn + WriteXDR>(e: E) -> RawCallError {
390        RawCallError::Exception(caught(e))
391    }
392
393    fn classify(e: RawCallError) -> alloc::string::String {
394        use alloc::string::ToString;
395        match_exn!(e, {
396            Alpha::Big { n } if n > 100 => "alpha-big-huge".to_string(),
397            Alpha::Big { n } => alloc::format!("alpha-big-{n}"),
398            Alpha::NotFound => "alpha-not-found".to_string(),
399            Beta::Rejected => "beta-rejected".to_string(),
400            LibraryExn::NonNegativeIntOverflow => "lib-overflow".to_string(),
401            _ => alloc::format!("other:{e:?}"),
402        })
403    }
404
405    #[test]
406    fn matches_each_type_by_selector() {
407        assert_eq!(classify(err(Alpha::NotFound)), "alpha-not-found");
408        assert_eq!(classify(err(Beta::Rejected)), "beta-rejected");
409        assert_eq!(
410            classify(err(LibraryExn::NonNegativeIntOverflow)),
411            "lib-overflow"
412        );
413    }
414
415    #[test]
416    fn guard_failure_falls_through_to_a_later_arm_not_to_wildcard() {
417        assert_eq!(classify(err(Alpha::Big { n: 500 })), "alpha-big-huge");
418        assert_eq!(classify(err(Alpha::Big { n: 7 })), "alpha-big-7");
419    }
420
421    #[test]
422    fn non_exception_failures_reach_the_wildcard() {
423        assert!(classify(RawCallError::Invalid).starts_with("other:"));
424        assert!(
425            classify(RawCallError::Fault(FaultInfo::empty()))
426                .starts_with("other:")
427        );
428    }
429
430    #[test]
431    fn unrecognised_selector_reaches_the_wildcard() {
432        let raw = RawException::new(!0u64, b"whatever");
433        assert!(classify(RawCallError::Exception(raw)).starts_with("other:"));
434    }
435
436    /// `try_decode` separates the two situations `try_as` reports alike.
437    #[test]
438    fn try_decode_separates_wrong_selector_from_bad_payload() {
439        // Selector belongs to another type.
440        let other = err(Beta::Rejected);
441        assert!(other.try_decode::<Alpha>().is_none());
442        assert_eq!(other.try_as::<Alpha>(), None);
443
444        // Matched and decoded.
445        let good = err(Alpha::NotFound);
446        assert!(matches!(
447            good.try_decode::<Alpha>(),
448            Some(Ok(Alpha::NotFound))
449        ));
450
451        // Alpha's selector over a payload that is not an Alpha. `try_as`
452        // cannot tell this from the first case; `try_decode` can.
453        let bad = RawCallError::Exception(RawException::new(
454            Alpha::SELECTOR,
455            &encode_xdr(&9_999_999i32).unwrap(),
456        ));
457        assert_eq!(bad.try_as::<Alpha>(), None);
458        assert!(matches!(bad.try_decode::<Alpha>(), Some(Err(_))));
459    }
460
461    /// The routine skew case behind the docs' warning: a callee adds an enum
462    /// variant, which is semver-compatible and leaves the selector alone, so
463    /// an older caller receives its own selector over a discriminant it does
464    /// not know.
465    #[test]
466    fn try_decode_reports_an_unknown_variant_under_a_shared_selector() {
467        // `AlphaPlus` stands in for a later version of `Alpha`: same
468        // selector, one extra variant.
469        #[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
470        #[repr(i32)]
471        enum AlphaPlus {
472            NotFound = 1,
473            Big { n: u64 } = 2,
474            AddedLater = 3,
475        }
476
477        impl Exn for AlphaPlus {
478            const SELECTOR: u64 = Alpha::SELECTOR;
479        }
480
481        let newer = RawCallError::Exception(RawException::new(
482            AlphaPlus::SELECTOR,
483            &encode_xdr(&AlphaPlus::AddedLater).unwrap(),
484        ));
485
486        // The older caller sees a decode failure, not an unrelated error.
487        assert_eq!(newer.try_as::<Alpha>(), None);
488        assert!(matches!(newer.try_decode::<Alpha>(), Some(Err(_))));
489
490        // And `match_exn!` sweeps it into `_`, which is the documented
491        // behavior this test pins rather than endorses.
492        let landed = match_exn!(newer, {
493            Alpha::NotFound => "not-found",
494            Alpha::Big { .. } => "big",
495            _ => "wildcard",
496        });
497        assert_eq!(landed, "wildcard");
498    }
499
500    /// Decoding reads a prefix, so trailing bytes are accepted rather than
501    /// reported. Pins the asymmetry the macro docs call out.
502    #[test]
503    fn try_decode_accepts_a_payload_with_trailing_bytes() {
504        let mut payload = encode_xdr(&Alpha::NotFound).unwrap();
505        payload.extend_from_slice(b"appended-later");
506        let padded = RawCallError::Exception(RawException::new(
507            Alpha::SELECTOR,
508            &payload,
509        ));
510
511        assert!(matches!(
512            padded.try_decode::<Alpha>(),
513            Some(Ok(Alpha::NotFound))
514        ));
515        assert_eq!(padded.try_as::<Alpha>(), Some(Alpha::NotFound));
516    }
517
518    /// A failure carrying no exception is `None`, same as a mismatched
519    /// selector; `CallError::Exception` is what separates those.
520    #[test]
521    fn try_decode_is_none_when_no_exception_is_carried() {
522        let fault: RawCallError = RawCallError::Fault(FaultInfo::empty());
523        assert!(fault.try_decode::<Alpha>().is_none());
524        let invalid: RawCallError = RawCallError::Invalid;
525        assert!(invalid.try_decode::<Alpha>().is_none());
526    }
527
528    #[test]
529    fn undecodable_payload_of_a_matching_selector_reaches_the_wildcard() {
530        // Right selector, payload truncated past recognition.
531        let raw = RawException::new(Alpha::SELECTOR, &[]);
532        assert!(classify(RawCallError::Exception(raw)).starts_with("other:"));
533    }
534
535    #[test]
536    fn matches_an_owned_raw_exception_directly() {
537        let raw = caught(Beta::Rejected);
538        let got = match_exn!(raw, {
539            Beta::Rejected => 1u32,
540            _ => 0,
541        });
542        assert_eq!(got, 1);
543    }
544
545    #[test]
546    fn alternates_within_one_type_and_at_binding() {
547        let got = match_exn!(err(Alpha::Big { n: 3 }), {
548            Alpha::NotFound | Alpha::Big { .. } => 1u32,
549            _ => 0,
550        });
551        assert_eq!(got, 1);
552
553        let bound = match_exn!(err(Alpha::NotFound), {
554            v @ Alpha::NotFound => alloc::format!("{v:?}"),
555            _ => alloc::string::String::new(),
556        });
557        assert_eq!(bound, "NotFound");
558    }
559
560    #[test]
561    fn arms_are_tried_in_order_first_match_wins() {
562        let got = match_exn!(err(Alpha::NotFound), {
563            Alpha::NotFound => 1u32,
564            Alpha::NotFound => 2,
565            _ => 0,
566        });
567        assert_eq!(got, 1);
568    }
569
570    /// A diverging arm body leaves the `break` that would carry its value
571    /// unreachable. That must not warn (these crates build with
572    /// `-D warnings`).
573    #[test]
574    #[should_panic(expected = "diverging arm")]
575    fn diverging_arm_bodies_do_not_warn() {
576        let _: u32 = match_exn!(err(Beta::Rejected), {
577            Alpha::NotFound => 1,
578            Beta::Rejected => panic!("diverging arm"),
579            _ => 0,
580        });
581    }
582
583    #[test]
584    fn diverging_wildcard_body_does_not_warn() {
585        let got = match_exn!(err(Beta::Rejected), {
586            Beta::Rejected => 1u32,
587            _ => panic!("unreachable in this case"),
588        });
589        assert_eq!(got, 1);
590    }
591
592    /// The documented propagate idiom is `_ => e.re_throw()`, which *moves*
593    /// the scrutinee the macro holds a borrow of — NLL has to see that borrow
594    /// end before the `_` body. `re_throw` aborts, so consume by value here
595    /// instead; `compile-pass/match_exn_rethrow.rs` pins the real spelling.
596    #[test]
597    fn wildcard_arm_moves_the_named_scrutinee() {
598        fn consume(e: RawCallError) -> u32 {
599            match e {
600                RawCallError::Exception(_) => 7,
601                _ => 0,
602            }
603        }
604
605        let e = err(Alpha::NotFound);
606        let got = match_exn!(e, {
607            Beta::Rejected => 1u32,
608            _ => consume(e),
609        });
610        assert_eq!(got, 7);
611    }
612
613    #[test]
614    fn wildcard_only_is_legal() {
615        let got = match_exn!(err(Alpha::NotFound), { _ => 5u32 });
616        assert_eq!(got, 5);
617    }
618
619    #[test]
620    fn block_bodies_need_no_trailing_comma() {
621        let got = match_exn!(err(Beta::Rejected), {
622            Alpha::NotFound => { 1u32 }
623            Beta::Rejected => { 2 }
624            _ => { 0 }
625        });
626        assert_eq!(got, 2);
627    }
628
629    /// A block body may be *followed* by a comma, the way `match` arms are
630    /// usually written. Regression test: collapsing the comma-required and
631    /// comma-free block rules into one `$(,)? $($rest:tt)*` matcher turns
632    /// this into `error: local ambiguity when calling macro
633    /// `__match_exn_arms``, because a present comma could belong either to
634    /// the optional repetition or to `$rest`.
635    #[test]
636    fn block_body_then_comma_plain_arm() {
637        let got = match_exn!(err(Beta::Rejected), {
638            Alpha::NotFound => { 1u32 },
639            Beta::Rejected => { 2 },
640            _ => 0,
641        });
642        assert_eq!(got, 2);
643    }
644
645    /// The guarded rule was collapsed the same way, so it needs the same
646    /// cover. The guard must also still fall through to a later arm.
647    #[test]
648    fn block_body_then_comma_guarded_arm() {
649        let classify = |e: RawCallError| {
650            match_exn!(e, {
651                Alpha::Big { n } if n > 100 => { 1u32 },
652                Alpha::Big { n } if n > 10 => { 2 },
653                Alpha::Big { .. } => { 3 },
654                _ => 0,
655            })
656        };
657        assert_eq!(classify(err(Alpha::Big { n: 500 })), 1);
658        assert_eq!(classify(err(Alpha::Big { n: 50 })), 2);
659        assert_eq!(classify(err(Alpha::Big { n: 5 })), 3);
660        assert_eq!(classify(RawCallError::Invalid), 0);
661    }
662
663    /// Body shape and trailing comma are independent: every combination has
664    /// to parse, mixed freely within one invocation.
665    #[test]
666    fn body_shapes_and_commas_mix_freely() {
667        let got = match_exn!(err(Beta::Rejected), {
668            Alpha::NotFound => 1u32,
669            Alpha::Big { .. } => { 2 },
670            Beta::Rejected => { 3 }
671            _ => 0,
672        });
673        assert_eq!(got, 3);
674
675        // Same arms, with the match falling through to `_` as a block.
676        let no_exn: RawCallError = RawCallError::Invalid;
677        let fell_through = match_exn!(no_exn, {
678            Alpha::NotFound => 1u32,
679            Beta::Rejected => { 2 },
680            _ => { 0 }
681        });
682        assert_eq!(fell_through, 0);
683    }
684
685    /// A block body on the last typed arm, comma'd, with `_` immediately
686    /// after — the shape that appears in a two-arm handler, and the one most
687    /// likely to be written by hand.
688    #[test]
689    fn block_body_then_comma_before_wildcard() {
690        let got = match_exn!(err(Alpha::NotFound), {
691            Alpha::NotFound => { 7u32 },
692            _ => 0,
693        });
694        assert_eq!(got, 7);
695    }
696}