Skip to content

Commit

Permalink
Add throws_expr! for wrapping closures/async blocks
Browse files Browse the repository at this point in the history
Also `#[throws]` should now work on closures/async blocks with the nightly feature active
  • Loading branch information
Nemo157 committed Sep 21, 2023
1 parent 8a90f93 commit e445364
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 9 deletions.
14 changes: 10 additions & 4 deletions macros/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Args> {
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]) {
Expand Down
5 changes: 5 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ pub fn throws(args: TokenStream, input: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(args as Args);
Throws::new(args).fold(input)
}

#[proc_macro]
pub fn throws_expr(input: TokenStream) -> TokenStream {
Throws::new(Args::default()).fold(input)
}
55 changes: 50 additions & 5 deletions macros/src/throws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -143,7 +175,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)
Expand All @@ -154,6 +186,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()
}
Expand Down
33 changes: 33 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,46 @@
//! 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 is a separate non-attribute macro
//! [`throws_expr!`] available to wrap closures or async blocks.
//!
//! ## Example
//!
//! ```
//! use std::io::{self, Read, Error};
//!
//! use culpa::{throw, throws_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!");
//! });
//! ```
#[doc(inline)]
/// Annotates a function that "throws" a Result.
///
/// See the main crate docs for more details.
pub use culpa_macros::throws;

#[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;

/// Throw an error.
///
/// This macro is equivalent to `Err($err)?`.
Expand Down
87 changes: 87 additions & 0 deletions tests/async_block.rs
Original file line number Diff line number Diff line change
@@ -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::<i32, ()>::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: std::future::Future>(f: F) -> F::Output {
struct NoopWake;
impl std::task::Wake for NoopWake {
fn wake(self: std::sync::Arc<Self>) {}
}
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
}
76 changes: 76 additions & 0 deletions tests/closure.rs
Original file line number Diff line number Diff line change
@@ -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::<i32, ()>::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)());
})(),
);
}

0 comments on commit e445364

Please sign in to comment.