try_let/lib.rs
1//! This is an implementation of a `try-let` similar to the one proposed in
2//! [RFC #1303](https://github.com/rust-lang/rfcs/pull/1303), as a proc macro.
3//!
4//! > _NOTE:_ Proc macros in statement position are currently unstable, meaning this
5//! > macro will not work on stable rust until
6//! > [PR #68717](https://github.com/rust-lang/rust/pull/68717) is merged.
7//!
8//! # Usage
9//!
10//! try-let is implemented using a proc macro instead, as parsing the pattern
11//! expression in the way which try-let needs to is not possible with a
12//! `macro_rules!` macro.
13//!
14//! This plugin currently requires enabling `#![feature(proc_macro_hygiene)]`, like
15//! so:
16//!
17//! ```rust
18//! #![feature(proc_macro_hygiene)]
19//! use try_let::try_let;
20//! ```
21//!
22//! The actual use is fairly similar to a `let` expression:
23//!
24//! ```rust
25//! # #![feature(proc_macro_hygiene)]
26//! # use try_let::try_let;
27//! # fn main() -> Result<(), &'static str> {
28//! # let foo = Some(());
29//! try_let!(Some(x) = foo else {
30//! return Err("Shoot! There was a problem!")
31//! });
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! The expression after else must diverge (e.g. via `return`, `continue`, `break`
37//! or `panic!`).
38//!
39//! This also handles more complex types than `Some` and `None`:
40//!
41//! ```rust
42//! # #![feature(proc_macro_hygiene)]
43//! # use try_let::try_let;
44//! # fn main() {
45//! enum E {
46//! A(i32, i32, i32, i32, Option<i32>, Result<(), i32>),
47//! B,
48//! }
49//!
50//! let foo = E::A(0, 21, 10, 34, Some(5), Err(32));
51//!
52//! try_let!(E::A(a, 21, c, 34, Some(e), Err(f)) = foo else {
53//! unreachable!()
54//! });
55//! // a, c, e, and f are all bound here.
56//! assert_eq!(a, 0);
57//! assert_eq!(c, 10);
58//! assert_eq!(e, 5);
59//! assert_eq!(f, 32);
60//! # }
61//! ```
62//!
63//! # Why
64//!
65//! This provides a simple way to avoid the rightward-shift of logic which performs
66//! a large number of faillible pattern matches in rust. This allows the main logic
67//! flow to continue without increasing the indent level, while handling errors with
68//! diverging logic.
69//!
70//! # How
71//!
72//! a `try_let!()` invocation expands to the following:
73//!
74//! ```rust
75//! # #![feature(proc_macro_hygiene)]
76//! # use try_let::try_let;
77//! # fn main() -> Result<(), &'static str> {
78//! # let foo = Some(10);
79//! try_let!(Some(x) = foo else {
80//! return Err("Shoot! There was a problem!");
81//! });
82//! // ... becomes ...
83//! let (x,) = match foo {
84//! Some(x) => (x,),
85//! _ => {
86//! return Err("Shoot! There was a problem!");
87//! }
88//! };
89//! # Ok(())
90//! # }
91//! ```
92//!
93//! ## A note on `None` and empty enum variants
94//!
95//! A question which some people will be asking now is how are enum variants like
96//! `None` handled?
97//!
98//! ```rust
99//! # #![feature(proc_macro_hygiene)]
100//! # use try_let::try_let;
101//! # fn main() {
102//! # let foo = Some(10);
103//! try_let!(None = foo else {
104//! return;
105//! });
106//! // ... becomes ...
107//! let () = match foo {
108//! None => (),
109//! _ => {
110//! return;
111//! }
112//! };
113//! # }
114//! ```
115//!
116//! `None` isn't mistaken for a binding variable by try-let because of the dirty
117//! little trick which try-let uses to function: which is that it is powered by
118//! rust's style conventions. There is no way for the parser (which is all that the
119//! syntax extension has access to) to determine whether a lone identifier in a
120//! pattern is an empty enum variant like `None` or a variable binding like `x`.
121//! This is determined later in the compiler, too late for this extension to use
122//! that information.
123//!
124//! Instead, the extension checks the first character of the identifier. If it is an
125//! ASCII capital, we assume it is a empty enum variant, and otherwise we assume it
126//! is a variable binding.
127
128use quote::quote;
129use syn::parse::{Parse, ParseStream};
130use syn::visit::{self, Visit};
131use syn::{parse_macro_input, Block, Expr, Ident, Pat, PatIdent, Result, Token};
132
133struct TryLet {
134 pat: Pat,
135 _eq_token: Token![=],
136 expr: Expr,
137 _else_token: Token![else],
138 fallback: Block,
139}
140
141impl Parse for TryLet {
142 fn parse(input: ParseStream) -> Result<Self> {
143 Ok(TryLet {
144 pat: input.parse()?,
145 _eq_token: input.parse()?,
146 expr: input.parse()?,
147 _else_token: input.parse()?,
148 fallback: input.parse()?,
149 })
150 }
151}
152
153struct Visitor<'a> {
154 bindings: &'a mut Vec<Ident>,
155}
156
157impl<'a> Visit<'_> for Visitor<'a> {
158 fn visit_pat_ident(&mut self, id: &PatIdent) {
159 // If the identifier starts with an uppercase letter, assume that it's a
160 // constant, unit struct, or a unit enum variant. This isn't a very
161 // accurate system for this type of check, but is unfortunately the best
162 // we can do from within a proc macro.
163 if id
164 .ident
165 .to_string()
166 .chars()
167 .next()
168 .unwrap()
169 .is_ascii_uppercase()
170 {
171 return;
172 }
173 self.bindings.push(id.ident.clone());
174
175 // Visit any nested expressions (e.g. using `id @ pat`)
176 visit::visit_pat_ident(self, id);
177 }
178}
179
180/// The whole point
181///
182/// See the module-level documentation for details.
183#[proc_macro]
184pub fn try_let(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
185 let TryLet {
186 pat,
187 expr,
188 fallback,
189 ..
190 } = parse_macro_input!(input as TryLet);
191
192 let mut bindings = Vec::new();
193 Visitor {
194 bindings: &mut bindings,
195 }
196 .visit_pat(&pat);
197
198 // NOTE: This doesn't use `mixed_site`, however it also doesn't introduce
199 // any new identifiers not from the call-site.
200 let output = quote!(
201 let (#(#bindings,)*) = match (#expr) {
202 #pat => (#(#bindings,)*),
203 _ => {
204 #fallback;
205 }
206 };
207 );
208 return output.into();
209}