diff --git a/macros/src/args.rs b/macros/src/args.rs index b0a2285..32af54b 100644 --- a/macros/src/args.rs +++ b/macros/src/args.rs @@ -51,13 +51,19 @@ impl Args { } } +impl Default for Args { + fn default() -> Self { + Self { + error: Some(default_error()), + wrapper: Some(result()), + } + } +} + impl Parse for Args { fn parse(input: ParseStream) -> Result { if input.is_empty() { - return Ok(Args { - error: Some(default_error()), - wrapper: Some(result()), - }); + return Ok(Args::default()); } let error = match input.peek(Token![as]) { diff --git a/macros/src/lib.rs b/macros/src/lib.rs index dfd8868..3d51d6f 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -19,3 +19,13 @@ pub fn try_fn(args: TokenStream, input: TokenStream) -> TokenStream { assert!(args.to_string() == "", "try_fn does not take arguments"); Throws::new(None).fold(input) } + +#[proc_macro] +pub fn throws_expr(input: TokenStream) -> TokenStream { + Throws::new(Some(Args::default())).fold(input) +} + +#[proc_macro] +pub fn try_expr(input: TokenStream) -> TokenStream { + Throws::new(None).fold(input) +} diff --git a/macros/src/throws.rs b/macros/src/throws.rs index 1c7f9cc..dcd23a1 100644 --- a/macros/src/throws.rs +++ b/macros/src/throws.rs @@ -32,11 +32,17 @@ impl Throws { } else if let Ok(impl_item_fn) = syn::parse(input.clone()) { let impl_item_fn = self.fold_impl_item_fn(impl_item_fn); quote::quote!(#impl_item_fn).into() - } else if let Ok(trait_item_fn) = syn::parse(input) { + } else if let Ok(trait_item_fn) = syn::parse(input.clone()) { let trait_item_fn = self.fold_trait_item_fn(trait_item_fn); quote::quote!(#trait_item_fn).into() + } else if let Ok(expr_closure) = syn::parse(input.clone()) { + let expr_closure = self.fold_expr_closure(expr_closure); + quote::quote!(#expr_closure).into() + } else if let Ok(expr_async) = syn::parse(input) { + let expr_async = self.fold_expr_async(expr_async); + quote::quote!(#expr_async).into() } else { - panic!("#[throws] attribute can only be applied to functions and methods") + panic!("#[throws] attribute can only be applied to functions, methods, closures or async blocks") } } } @@ -99,11 +105,37 @@ impl Fold for Throws { } fn fold_expr_closure(&mut self, i: syn::ExprClosure) -> syn::ExprClosure { - i // TODO + if !self.outer_fn { + return i; + } + + let output = match i.output { + syn::ReturnType::Default => syn::parse_quote!(-> _), + output => output, + }; + let output = self.fold_return_type(output); + + self.outer_fn = false; + + let inner = self.fold_expr(*i.body); + let body = Box::new(make_fn_expr(&self.return_type, &inner)); + + syn::ExprClosure { output, body, ..i } } fn fold_expr_async(&mut self, i: syn::ExprAsync) -> syn::ExprAsync { - i // TODO + if !self.outer_fn { + return i; + } + + // update self.return_type + let _ = self.fold_return_type(syn::parse_quote!(-> _)); + self.outer_fn = false; + + let inner = self.fold_block(i.block); + let block = make_fn_block(&self.return_type, &inner); + + syn::ExprAsync { block, ..i } } fn fold_return_type(&mut self, i: syn::ReturnType) -> syn::ReturnType { @@ -147,7 +179,7 @@ fn make_fn_block(ty: &syn::Type, inner: &syn::Block) -> syn::Block { let mut block: syn::Block = syn::parse2(quote::quote! {{ #[allow(clippy::diverging_sub_expression)] { - let __ret = { #inner }; + let __ret = #inner; #[allow(unreachable_code)] <#ty as ::culpa::__internal::_Succeed>::from_ok(__ret) @@ -158,6 +190,19 @@ fn make_fn_block(ty: &syn::Type, inner: &syn::Block) -> syn::Block { block } +fn make_fn_expr(ty: &syn::Type, inner: &syn::Expr) -> syn::Expr { + syn::parse2(quote::quote! {{ + #[allow(clippy::diverging_sub_expression)] + { + let __ret = { #inner }; + + #[allow(unreachable_code)] + <#ty as ::culpa::__internal::_Succeed>::from_ok(__ret) + } + }}) + .unwrap() +} + fn ok(ty: &syn::Type, expr: &syn::Expr) -> syn::Expr { syn::parse2(quote::quote!(<#ty as ::culpa::__internal::_Succeed>::from_ok(#expr))).unwrap() } diff --git a/src/lib.rs b/src/lib.rs index f7d6c36..bcbe35b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,48 @@ //! support `throws` syntax on functions that return `Poll` (so you can't use this syntax when //! implementing a `Future` by hand, for example). I hope to come up with a way to support `Poll` //! in the future. +//! +//! # Annotating Expressions +//! +//! Attributes on expressions are still unstable, so there are separate non-attribute macro +//! [`throws_expr!`] and [`try_expr!`] available to wrap closures or async blocks. `throws_expr!` +//! does not have any way to pass an error type, so only supports usage with a contextual "default +//! error type". If you must override it for a single closure, you can do so by putting it in a +//! block with a separate `use` or type alias, or simply use `try_expr!` with a normal closure +//! return type annotation. +//! +//! ## Example +//! +//! ``` +//! use std::io::{self, Read, Error}; +//! +//! use culpa::{throw, throws_expr, try_expr}; +//! +//! let closure = throws_expr!(|| { +//! let mut file = std::fs::File::open("The_House_of_the_Spirits.txt")?; +//! let mut text = String::new(); +//! file.read_to_string(&mut text)?; +//! +//! if !text.starts_with("Barrabas came to us by sea, the child Clara wrote") { +//! throw!(Error::from_raw_os_error(22)); +//! } +//! +//! println!("Okay!"); +//! }); +//! +//! let string_throwing_closure = { +//! type Error = &'static str; +//! throws_expr!(|| throw!("This is not for you.")) +//! }; +//! +//! let string_throwing_closure = try_expr!(|| -> Result<_, &'static str> { +//! throw!("The air trembles. A breath of change passes…") +//! }); +//! +//! let maybe_random = try_expr!(|| -> Option<_> { +//! 4 +//! }); +//! ``` #[doc(inline)] /// Annotates a function that "throws" a Result. @@ -126,6 +168,22 @@ pub use culpa_macros::throws; /// See the main crate docs for more details. pub use culpa_macros::try_fn; +#[doc(inline)] +/// Annotates an expression (closure or async block) that "throws" a Result. +/// +/// Workaround for attributes on expressions being unstable. +/// +/// See the main crate docs for more details. +pub use culpa_macros::throws_expr; + +#[doc(inline)] +/// Annotates an expression (closure or async block) that implicitly wraps a try block. +/// +/// Workaround for attributes on expressions being unstable. +/// +/// See the main crate docs for more details. +pub use culpa_macros::try_expr; + /// Throw an error. /// /// This macro is equivalent to `Err($err)?`. diff --git a/tests/throws_expr_async_block.rs b/tests/throws_expr_async_block.rs new file mode 100644 index 0000000..38b06c3 --- /dev/null +++ b/tests/throws_expr_async_block.rs @@ -0,0 +1,87 @@ +use culpa::{throw, throws_expr}; + +#[test] +#[allow(clippy::unused_unit)] +fn unit() { + type Error = (); + let ok = Result::<(), ()>::Ok(()); + assert_eq!(ok, poll(throws_expr!(async {}))); + assert_eq!(ok, poll(throws_expr!(async { () }))); + assert_eq!( + ok, + poll(throws_expr!(async { + return; + })) + ); + assert_eq!( + ok, + poll(throws_expr!(async { + return (); + })) + ); +} + +#[test] +fn integer() { + type Error = (); + let ok = Result::::Ok(1); + assert_eq!(ok, poll(throws_expr!(async { 1 }))); + assert_eq!( + ok, + poll(throws_expr!(async { + return 1; + })) + ); +} + +#[test] +fn throws_unit() { + type Error = (); + let err = Result::<(), ()>::Err(()); + assert_eq!(err, poll(throws_expr!(async { throw!(()) }))); +} + +#[test] +fn throws_integer() { + type Error = i32; + let err = Result::<(), i32>::Err(1); + assert_eq!(err, poll(throws_expr!(async { throw!(1) }))); +} + +#[test] +fn has_inner_fn() { + type Error = (); + assert_eq!( + Result::<(), ()>::Ok(()), + poll(throws_expr!(async { + async fn foo() -> i32 { + 5 + } + assert_eq!(5, foo().await); + })), + ); +} + +#[test] +fn has_inner_closure() { + type Error = (); + assert_eq!( + Result::<(), ()>::Ok(()), + poll(throws_expr!(async { + assert_eq!(5, async { 5 }.await); + })), + ); +} + +fn poll(f: F) -> F::Output { + struct NoopWake; + impl std::task::Wake for NoopWake { + fn wake(self: std::sync::Arc) {} + } + let std::task::Poll::Ready(output) = std::pin::pin!(f).poll( + &mut std::task::Context::from_waker(&std::sync::Arc::new(NoopWake).into()), + ) else { + panic!("future was not ready") + }; + output +} diff --git a/tests/throws_expr_closure.rs b/tests/throws_expr_closure.rs new file mode 100644 index 0000000..74b1f5b --- /dev/null +++ b/tests/throws_expr_closure.rs @@ -0,0 +1,76 @@ +#![allow(unused_braces)] +#![allow(clippy::redundant_closure_call)] + +use culpa::{throw, throws_expr}; + +#[test] +#[rustfmt::skip] +fn unit() { + type Error = (); + let ok = Result::<(), ()>::Ok(()); + assert_eq!(ok, throws_expr!(|| {})()); + assert_eq!(ok, throws_expr!(|| ())()); + assert_eq!(ok, throws_expr!(|| -> () {})()); + assert_eq!(ok, throws_expr!(|| { return; })()); + assert_eq!(ok, throws_expr!(|| { return (); })()); + assert_eq!(ok, throws_expr!(|| -> () { return; })()); + assert_eq!(ok, throws_expr!(|| -> () { return (); })()); +} + +#[test] +#[rustfmt::skip] +fn integer() { + type Error = (); + let ok = Result::::Ok(1); + assert_eq!(ok, throws_expr!(|| { 1 })()); + assert_eq!(ok, throws_expr!(|| 1)()); + assert_eq!(ok, throws_expr!(|| -> i32 { 1 })()); + assert_eq!(ok, throws_expr!(|| { return 1; })()); + assert_eq!(ok, throws_expr!(|| -> i32 { return 1; })()); + assert_eq!(ok, throws_expr!(|| -> _ { 1 })()); +} + +#[test] +#[rustfmt::skip] +fn throws_unit() { + type Error = (); + let err = Result::<(), ()>::Err(()); + assert_eq!(err, throws_expr!(|| { throw!(()) })()); + assert_eq!(err, throws_expr!(|| throw!(()))()); + assert_eq!(err, throws_expr!(|| -> () { throw!(()) })()); +} + +#[test] +#[rustfmt::skip] +fn throws_integer() { + type Error = i32; + let err = Result::<(), i32>::Err(1); + assert_eq!(err, throws_expr!(|| { throw!(1)} )()); + assert_eq!(err, throws_expr!(|| throw!(1))()); + assert_eq!(err, throws_expr!(|| -> () { throw!(1) })()); +} + +#[test] +fn has_inner_fn() { + type Error = (); + assert_eq!( + Result::<(), ()>::Ok(()), + throws_expr!(|| { + fn foo() -> i32 { + 5 + } + assert_eq!(5, foo()); + })(), + ); +} + +#[test] +fn has_inner_closure() { + type Error = (); + assert_eq!( + Result::<(), ()>::Ok(()), + throws_expr!(|| { + assert_eq!(5, (|| 5)()); + })(), + ); +} diff --git a/tests/try_expr_async_block.rs b/tests/try_expr_async_block.rs new file mode 100644 index 0000000..0553d3f --- /dev/null +++ b/tests/try_expr_async_block.rs @@ -0,0 +1,119 @@ +use culpa::{throw, try_expr}; + +#[test] +#[allow(clippy::unused_unit)] +fn unit() { + let ok = Result::<(), ()>::Ok(()); + assert_eq!(ok, poll(try_expr!(async {}))); + assert_eq!(ok, poll(try_expr!(async { () }))); + assert_eq!( + ok, + poll(try_expr!(async { + return; + })) + ); + assert_eq!( + ok, + poll(try_expr!(async { + return (); + })) + ); +} + +#[test] +fn integer() { + let ok = Result::::Ok(1); + assert_eq!(ok, poll(try_expr!(async { 1 }))); + assert_eq!( + ok, + poll(try_expr!(async { + return 1; + })) + ); +} + +#[test] +fn try_unit() { + let err = Result::<(), ()>::Err(()); + assert_eq!(err, poll(try_expr!(async { throw!(()) }))); +} + +#[test] +fn try_integer() { + let err = Result::<(), i32>::Err(1); + assert_eq!(err, poll(try_expr!(async { throw!(1) }))); +} + +#[test] +fn has_inner_fn() { + assert_eq!( + Result::<(), ()>::Ok(()), + poll(try_expr!(async { + async fn foo() -> i32 { + 5 + } + assert_eq!(5, foo().await); + })), + ); +} + +#[test] +fn has_inner_closure() { + assert_eq!( + Result::<(), ()>::Ok(()), + poll(try_expr!(async { + assert_eq!(5, async { 5 }.await); + })), + ); +} + +fn poll(f: F) -> F::Output { + struct NoopWake; + impl std::task::Wake for NoopWake { + fn wake(self: std::sync::Arc) {} + } + let std::task::Poll::Ready(output) = std::pin::pin!(f).poll( + &mut std::task::Context::from_waker(&std::sync::Arc::new(NoopWake).into()), + ) else { + panic!("future was not ready") + }; + output +} + +#[test] +#[allow(clippy::unused_unit)] +fn option_unit() { + let some = Option::<()>::Some(()); + assert_eq!(some, poll(try_expr!(async {}))); + assert_eq!(some, poll(try_expr!(async { () }))); + assert_eq!( + some, + poll(try_expr!(async { + return; + })) + ); + assert_eq!( + some, + poll(try_expr!(async { + return (); + })) + ); +} + +#[test] +fn option_integer() { + let some = Option::::Some(1); + assert_eq!(some, poll(try_expr!(async { 1 }))); + assert_eq!( + some, + poll(try_expr!(async { + return 1; + })) + ); +} + +#[test] +fn option_throws() { + let none = Option::<()>::None; + assert_eq!(none, poll(try_expr!(async { throw!() }))); +} diff --git a/tests/try_expr_closure.rs b/tests/try_expr_closure.rs new file mode 100644 index 0000000..4f3ac19 --- /dev/null +++ b/tests/try_expr_closure.rs @@ -0,0 +1,104 @@ +#![allow(unused_braces)] +#![allow(clippy::redundant_closure_call)] + +use culpa::{throw, try_expr}; + +#[test] +#[rustfmt::skip] +fn unit() { + let ok = Result::<(), ()>::Ok(()); + assert_eq!(ok, try_expr!(|| {})()); + assert_eq!(ok, try_expr!(|| ())()); + assert_eq!(ok, try_expr!(|| -> Result<(), ()> {})()); + assert_eq!(ok, try_expr!(|| { return; })()); + assert_eq!(ok, try_expr!(|| { return (); })()); + assert_eq!(ok, try_expr!(|| -> Result<(), ()> { return; })()); + assert_eq!(ok, try_expr!(|| -> Result<(), ()> { return (); })()); +} + +#[test] +#[rustfmt::skip] +fn integer() { + let ok = Result::::Ok(1); + assert_eq!(ok, try_expr!(|| { 1 })()); + assert_eq!(ok, try_expr!(|| 1)()); + assert_eq!(ok, try_expr!(|| -> Result { 1 })()); + assert_eq!(ok, try_expr!(|| { return 1; })()); + assert_eq!(ok, try_expr!(|| -> Result { return 1; })()); + assert_eq!(ok, try_expr!(|| -> _ { 1 })()); +} + +#[test] +#[rustfmt::skip] +fn throws_unit() { + let err = Result::<(), ()>::Err(()); + assert_eq!(err, try_expr!(|| { throw!(()) })()); + assert_eq!(err, try_expr!(|| throw!(()))()); + assert_eq!(err, try_expr!(|| -> Result<(), ()> { throw!(()) })()); +} + +#[test] +#[rustfmt::skip] +fn throws_integer() { + let err = Result::<(), i32>::Err(1); + assert_eq!(err, try_expr!(|| { throw!(1)} )()); + assert_eq!(err, try_expr!(|| throw!(1))()); + assert_eq!(err, try_expr!(|| -> Result<(), i32> { throw!(1) })()); +} + +#[test] +fn has_inner_fn() { + assert_eq!( + Result::<(), ()>::Ok(()), + try_expr!(|| { + fn foo() -> i32 { + 5 + } + assert_eq!(5, foo()); + })(), + ); +} + +#[test] +fn has_inner_closure() { + assert_eq!( + Result::<(), ()>::Ok(()), + try_expr!(|| { + assert_eq!(5, (|| 5)()); + })(), + ); +} + +#[test] +#[rustfmt::skip] +fn option_unit() { + let some = Option::<()>::Some(()); + assert_eq!(some, try_expr!(|| {})()); + assert_eq!(some, try_expr!(|| ())()); + assert_eq!(some, try_expr!(|| -> Option<()> {})()); + assert_eq!(some, try_expr!(|| { return; })()); + assert_eq!(some, try_expr!(|| { return (); })()); + assert_eq!(some, try_expr!(|| -> Option<()> { return; })()); + assert_eq!(some, try_expr!(|| -> Option<()> { return (); })()); +} + +#[test] +#[rustfmt::skip] +fn option_integer() { + let some = Option::::Some(1); + assert_eq!(some, try_expr!(|| { 1 })()); + assert_eq!(some, try_expr!(|| 1)()); + assert_eq!(some, try_expr!(|| -> Option { 1 })()); + assert_eq!(some, try_expr!(|| { return 1; })()); + assert_eq!(some, try_expr!(|| -> Option { return 1; })()); + assert_eq!(some, try_expr!(|| -> _ { 1 })()); +} + +#[test] +#[rustfmt::skip] +fn option_throws() { + let none = Option::<()>::None; + assert_eq!(none, try_expr!(|| { throw!() })()); + assert_eq!(none, try_expr!(|| throw!())()); + assert_eq!(none, try_expr!(|| -> Option<()> { throw!() })()); +}