diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 0f43fea..dfd8868 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -11,5 +11,11 @@ use throws::Throws; #[proc_macro_attribute] pub fn throws(args: TokenStream, input: TokenStream) -> TokenStream { let args = syn::parse_macro_input!(args as Args); - Throws::new(args).fold(input) + Throws::new(Some(args)).fold(input) +} + +#[proc_macro_attribute] +pub fn try_fn(args: TokenStream, input: TokenStream) -> TokenStream { + assert!(args.to_string() == "", "try_fn does not take arguments"); + Throws::new(None).fold(input) } diff --git a/macros/src/throws.rs b/macros/src/throws.rs index 27acd2d..1c7f9cc 100644 --- a/macros/src/throws.rs +++ b/macros/src/throws.rs @@ -11,13 +11,13 @@ use syn::fold::Fold; use crate::Args; pub struct Throws { - args: Args, + args: Option, outer_fn: bool, return_type: syn::Type, } impl Throws { - pub fn new(args: Args) -> Throws { + pub fn new(args: Option) -> Throws { Throws { args, outer_fn: true, @@ -110,9 +110,13 @@ impl Fold for Throws { if !self.outer_fn { return i; } - let return_type = self.args.ret(i); - let syn::ReturnType::Type(_, ty) = &return_type else { - unreachable!() + let return_type = match &mut self.args { + Some(args) => args.ret(i), + None => i, + }; + let ty = match &return_type { + syn::ReturnType::Type(_, ty) => (**ty).clone(), + syn::ReturnType::Default => syn::Type::Infer(syn::parse_quote!(_)), }; struct ImplTraitToInfer; impl Fold for ImplTraitToInfer { @@ -123,7 +127,7 @@ impl Fold for Throws { } } } - self.return_type = ImplTraitToInfer.fold_type(ty.as_ref().clone()); + self.return_type = ImplTraitToInfer.fold_type(ty); return_type } diff --git a/src/lib.rs b/src/lib.rs index b246601..f7d6c36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,25 @@ #![no_std] -//! Annotations a function that "throws" a Result. +//! Annotates a function that "throws" a Result. //! -//! Inside functions tagged with `throws`, you can use `?` and the `throw!` macro to return errors, -//! but you don't need to wrap the successful return values in `Ok`. +//! Inside functions tagged with either `throws` or `try_fn`, you can use `?` and the `throw!` +//! macro to return errors, but you don't need to wrap the successful return values in `Ok`. //! -//! Using this syntax, you can write fallible functions almost as if they were nonfallible. Every +//! Using this syntax, you can write fallible functions almost as if they were infallible. Every //! time a function call would return a `Result`, you "re-raise" the error using `?`, and if you //! wish to raise your own error, you can return it with the `throw!` macro. //! +//! The difference between `throws` and `try_fn` is in the function signature, with `throws` you +//! write the signature as if it were infallible too, it will be transformed into a `Result` for +//! you. With `try_fn` you write the signature as normal and only the body of the function will be +//! transformed. +//! //! ## Example +//! //! ``` //! use std::io::{self, Read}; //! -//! use culpa::{throw, throws}; +//! use culpa::{throw, throws, try_fn}; //! //! #[throws(io::Error)] //! fn check() { @@ -27,13 +33,26 @@ //! //! println!("Okay!"); //! } +//! +//! #[try_fn] +//! fn check_as_try_fn() -> std::io::Result<()> { +//! 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!(io::Error::from_raw_os_error(22)); +//! } +//! +//! println!("Okay!"); +//! } //! ``` //! -//! # Default Error Type +//! # `throws` Default Error Type //! -//! This macro supports a "default error type" - if you do not pass a type to the macro, it will -//! use the type named `Error` in this scope. So if you have defined an error type in this -//! module, that will be the error thrown by this function. +//! The `throws` macro supports a "default error type" - if you do not pass a type to the macro, it +//! will use the type named `Error` in the current scope. So if you have defined an error type in +//! the module, that will be the error thrown by this function. //! //! You can access this feature by omitting the arguments entirely or by passing `_` as the type. //! @@ -43,7 +62,7 @@ //! use culpa::throws; //! //! // Set the default error type for this module: -//! type Error = std::io::Error; +//! use std::io::Error; //! //! #[throws] //! fn print() { @@ -54,8 +73,9 @@ //! //! # Throwing as an Option //! -//! This syntax can also support functions which return an `Option` instead of a `Result`. The -//! way to access this is to pass `as Option` as the argument to `throw`. +//! This syntax can also support functions which return an `Option` instead of a `Result`. To use +//! this with `throws` pass `as Option` as the argument in place of the error type, to use it with +//! `try_fn` just put it as the return type like normal //! //! In functions that return `Option`, you can use the `throw!()` macro without any argument to //! return `None`. @@ -63,12 +83,18 @@ //! ## Example //! //! ``` -//! use culpa::{throw, throws}; -//! -//! #[throws(as Option)] +//! #[culpa::throws(as Option)] //! fn example(slice: &[T], needle: &T) -> usize { //! if !slice.contains(needle) { -//! throw!(); +//! culpa::throw!(); +//! } +//! slice.binary_search(needle).ok()? +//! } +//! +//! #[culpa::try_fn] +//! fn example_as_try_fn(slice: &[T], needle: &T) -> Option { +//! if !slice.contains(needle) { +//! culpa::throw!(); //! } //! slice.binary_search(needle).ok()? //! } @@ -78,15 +104,15 @@ //! //! The `?` syntax in Rust is controlled by a trait called `Try`, which is currently unstable. //! Because this feature is unstable and I don't want to maintain compatibility if its interface -//! changes, this crate currently only works with two stable `Try` types: Result and Option. +//! changes, this crate currently only works with two stable `Try` types: `Result` and `Option`. //! However, its designed so that it will hopefully support other `Try` types as well in the //! future. //! //! It's worth noting that `Try` also has some other stable implementations: specifically `Poll`. //! Because of the somewhat unusual implementation of `Try` for those types, this crate does not //! 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. +//! implementing a `Future` by hand, for example). I hope to come up with a way to support `Poll` +//! in the future. #[doc(inline)] /// Annotates a function that "throws" a Result. @@ -94,6 +120,12 @@ /// See the main crate docs for more details. pub use culpa_macros::throws; +#[doc(inline)] +/// Annotates a function that implicitly wraps a try block. +/// +/// See the main crate docs for more details. +pub use culpa_macros::try_fn; + /// Throw an error. /// /// This macro is equivalent to `Err($err)?`. @@ -215,3 +247,9 @@ pub mod __internal { /// } /// ``` const _DEAD_CODE: () = (); + +/// ```compile_fail +/// #[culpa::try_(())] +/// fn f() {} +/// ``` +const _NO_TRY_ARGS: () = (); diff --git a/tests/try.rs b/tests/try.rs new file mode 100644 index 0000000..7a526b2 --- /dev/null +++ b/tests/try.rs @@ -0,0 +1,165 @@ +use culpa::{throw, try_fn}; + +type Error = isize; + +#[try_fn] +pub fn unit_fn() -> Result<(), Error> {} + +#[try_fn] +pub fn returns_fn() -> Result { + return 0; +} + +#[try_fn] +pub fn returns_unit_fn() -> Result<(), Error> { + if true { + return; + } +} + +#[try_fn] +pub fn tail_returns_value() -> Result { + 0 +} + +#[try_fn] +pub async fn async_fn() -> Result<(), Error> {} + +#[try_fn] +pub async fn async_fn_with_ret() -> Result { + 0 +} + +#[try_fn] +pub fn throws_error() -> Result<(), i32> { + if true { + throw!(0); + } +} + +#[try_fn] +pub fn throws_and_has_return_type() -> Result<&'static str, i32> { + if true { + return "success"; + } else if false { + throw!(0); + } + "okay" +} + +#[try_fn] +pub fn throws_generics() -> Result<(), E> {} + +pub struct Foo; + +impl Foo { + #[try_fn] + pub fn static_method() -> Result<(), Error> {} + + #[try_fn] + pub fn bar(&self) -> Result { + if true { + return 1; + } + 0 + } +} + +#[try_fn] +pub fn has_inner_fn() -> Result<(), Error> { + fn inner_fn() -> i32 { + 0 + } + let _: i32 = inner_fn(); +} + +#[try_fn] +pub fn has_inner_closure() -> Result<(), Error> { + let f = || 0; + let _: i32 = f(); +} + +#[try_fn] +pub async fn has_inner_async_block() -> Result<(), Error> { + let f = async { 0 }; + let _: i32 = f.await; +} + +#[try_fn] +pub fn throws_as_result() -> Result { + 0 +} + +#[try_fn] +pub fn throws_as_result_alias() -> std::io::Result { + 0 +} + +#[try_fn] +pub fn ommitted_error() -> Result<(), Error> {} + +pub mod foo { + use culpa::{throw, try_fn}; + + pub type Error = i32; + + #[try_fn] + pub fn throws_integer() -> Result<(), i32> { + throw!(0); + } +} + +pub mod foo_trait_obj { + use culpa::try_fn; + pub trait FooTrait {} + + struct FooStruct; + + pub struct FooError; + impl FooTrait for FooStruct {} + + #[try_fn] + pub fn foo() -> Result, FooError> { + Box::new(FooStruct) + } +} + +#[try_fn] +pub fn let_else(a: Option) -> Result { + let Some(a) = a else { + return 0; + }; + a +} + +#[try_fn] +pub fn impl_trait() -> Result {} + +#[try_fn] +#[deny(unreachable_code)] +pub fn unreachable() -> Result<(), i32> { + todo!() +} + +trait Example { + #[try_fn] + fn foo() -> Result; +} + +#[try_fn] +fn as_option(x: bool) -> Option { + if x { + throw!(); + } + 0 +} + +#[test] +fn test_as_option_true() { + assert_eq!(None, as_option(true)); +} + +#[test] +fn test_as_option_false() { + assert_eq!(Some(0), as_option(false)) +}