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}