Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
16 changes: 10 additions & 6 deletions macros/src/throws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ use syn::fold::Fold;
use crate::Args;

pub struct Throws {
args: Args,
args: Option<Args>,
outer_fn: bool,
return_type: syn::Type,
}

impl Throws {
pub fn new(args: Args) -> Throws {
pub fn new(args: Option<Args>) -> Throws {
Throws {
args,
outer_fn: true,
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down
76 changes: 57 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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.
//!
Expand All @@ -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() {
Expand All @@ -54,21 +73,28 @@
//!
//! # 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`.
//!
//! ## Example
//!
//! ```
//! use culpa::{throw, throws};
//!
//! #[throws(as Option)]
//! #[culpa::throws(as Option)]
//! fn example<T: Eq + Ord>(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<T: Eq + Ord>(slice: &[T], needle: &T) -> Option<usize> {
//! if !slice.contains(needle) {
//! culpa::throw!();
//! }
//! slice.binary_search(needle).ok()?
//! }
Expand All @@ -78,22 +104,28 @@
//!
//! 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.
///
/// 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)?`.
Expand Down Expand Up @@ -215,3 +247,9 @@ pub mod __internal {
/// }
/// ```
const _DEAD_CODE: () = ();

/// ```compile_fail
/// #[culpa::try_(())]
/// fn f() {}
/// ```
const _NO_TRY_ARGS: () = ();
165 changes: 165 additions & 0 deletions tests/try.rs
Original file line number Diff line number Diff line change
@@ -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<i32, Error> {
return 0;
}

#[try_fn]
pub fn returns_unit_fn() -> Result<(), Error> {
if true {
return;
}
}

#[try_fn]
pub fn tail_returns_value() -> Result<i32, Error> {
0
}

#[try_fn]
pub async fn async_fn() -> Result<(), Error> {}

#[try_fn]
pub async fn async_fn_with_ret() -> Result<i32, Error> {
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<E>() -> Result<(), E> {}

pub struct Foo;

impl Foo {
#[try_fn]
pub fn static_method() -> Result<(), Error> {}

#[try_fn]
pub fn bar(&self) -> Result<i32, Error> {
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<i32, Error> {
0
}

#[try_fn]
pub fn throws_as_result_alias() -> std::io::Result<i32> {
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<Box<dyn FooTrait>, FooError> {
Box::new(FooStruct)
}
}

#[try_fn]
pub fn let_else(a: Option<u8>) -> Result<u8, Error> {
let Some(a) = a else {
return 0;
};
a
}

#[try_fn]
pub fn impl_trait() -> Result<impl std::fmt::Debug, Error> {}

#[try_fn]
#[deny(unreachable_code)]
pub fn unreachable() -> Result<(), i32> {
todo!()
}

trait Example {
#[try_fn]
fn foo() -> Result<i32, Error>;
}

#[try_fn]
fn as_option(x: bool) -> Option<i32> {
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))
}