const_fn/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3/*!
4A lightweight attribute for easy generation of const functions with conditional
5compilations.
6
7## Examples
8
9```rust
10use const_fn::const_fn;
11
12// function is `const` on specified version and later compiler (including beta, nightly, and dev build)
13#[const_fn("1.36")]
14pub const fn version() {
15    /* ... */
16}
17
18// function is `const` on nightly compiler (including dev build)
19#[const_fn(nightly)]
20pub const fn nightly() {
21    /* ... */
22}
23
24// function is `const` if `cfg(...)` is true
25# #[cfg(any())]
26#[const_fn(cfg(...))]
27# pub fn _cfg() { unimplemented!() }
28pub const fn cfg() {
29    /* ... */
30}
31
32// function is `const` if `cfg(feature = "...")` is true
33# #[cfg(any())]
34#[const_fn(feature = "...")]
35# pub fn _feature() { unimplemented!() }
36pub const fn feature() {
37    /* ... */
38}
39```
40
41### Use this crate as an optional dependency
42
43If no arguments are passed, `const_fn` will always make the function `const`.
44
45Therefore, you can use `const_fn` as an optional dependency by combination with `cfg_attr`.
46
47```rust
48// function is `const` if `cfg(feature = "...")` is true
49# #[cfg(any())]
50#[cfg_attr(feature = "...", const_fn::const_fn)]
51# pub fn _optional() { unimplemented!() }
52pub fn optional() {
53    /* ... */
54}
55```
56
57<!--
58TODO: document the behavior on the version on the nightly channel.
59      https://github.com/taiki-e/const_fn/issues/27
60      https://github.com/rust-lang/rust/pull/81468
61-->
62
63## Alternatives
64
65This crate is proc-macro, but is very lightweight, and has no dependencies.
66
67You can manually define declarative macros with similar functionality (see
68[`if_rust_version`](https://github.com/ogoffart/if_rust_version#examples)),
69or [you can define the same function twice with different cfg](https://github.com/crossbeam-rs/crossbeam/blob/0b6ea5f69fde8768c1cfac0d3601e0b4325d7997/crossbeam-epoch/src/atomic.rs#L340-L372).
70(Note: the former approach requires more macros to be defined depending on the
71number of version requirements, the latter approach requires more functions to
72be maintained manually)
73*/
74
75#![doc(test(
76    no_crate_inject,
77    attr(
78        deny(warnings, rust_2018_idioms, single_use_lifetimes),
79        allow(dead_code, unused_variables)
80    )
81))]
82#![forbid(unsafe_code)]
83
84// older compilers require explicit `extern crate`.
85#[allow(unused_extern_crates)]
86extern crate proc_macro;
87
88#[macro_use]
89mod error;
90
91mod ast;
92mod iter;
93mod to_tokens;
94mod utils;
95
96use std::str::FromStr;
97
98use proc_macro::{Delimiter, TokenStream, TokenTree};
99
100use crate::{
101    ast::LitStr,
102    error::{Error, Result},
103    iter::TokenIter,
104    to_tokens::ToTokens,
105    utils::{cfg_attrs, parse_as_empty, tt_span},
106};
107
108/// A lightweight attribute for easy generation of const functions with conditional compilations.
109///
110/// See crate level documentation for details.
111#[proc_macro_attribute]
112pub fn const_fn(args: TokenStream, input: TokenStream) -> TokenStream {
113    expand(args, input).unwrap_or_else(Error::into_compile_error)
114}
115
116fn expand(args: TokenStream, input: TokenStream) -> Result<TokenStream> {
117    let arg = parse_arg(args)?;
118    let mut func = ast::parse_input(input)?;
119    Ok(match arg {
120        Arg::Cfg(cfg) => {
121            let (mut tokens, cfg_not) = cfg_attrs(cfg);
122            tokens.extend(func.to_token_stream());
123            tokens.extend(cfg_not);
124            func.print_const = false;
125            tokens.extend(func.to_token_stream());
126            tokens
127        }
128        Arg::Feature(feat) => {
129            let (mut tokens, cfg_not) = cfg_attrs(feat);
130            tokens.extend(func.to_token_stream());
131            tokens.extend(cfg_not);
132            func.print_const = false;
133            tokens.extend(func.to_token_stream());
134            tokens
135        }
136        Arg::Version(req) => {
137            if req.major > 1
138                || req.minor + cfg!(const_fn_assume_incomplete_release) as u32 > VERSION.minor
139            {
140                func.print_const = false;
141            }
142            func.to_token_stream()
143        }
144        Arg::Nightly => {
145            func.print_const = VERSION.nightly;
146            func.to_token_stream()
147        }
148        Arg::Always => func.to_token_stream(),
149    })
150}
151
152enum Arg {
153    // `const_fn("...")`
154    Version(VersionReq),
155    // `const_fn(nightly)`
156    Nightly,
157    // `const_fn(cfg(...))`
158    Cfg(TokenStream),
159    // `const_fn(feature = "...")`
160    Feature(TokenStream),
161    // `const_fn`
162    Always,
163}
164
165fn parse_arg(tokens: TokenStream) -> Result<Arg> {
166    let iter = &mut TokenIter::new(tokens);
167
168    let next = iter.next();
169    let next_span = tt_span(next.as_ref());
170    match next {
171        None => return Ok(Arg::Always),
172        Some(TokenTree::Ident(i)) => match &*i.to_string() {
173            "nightly" => {
174                parse_as_empty(iter)?;
175                return Ok(Arg::Nightly);
176            }
177            "cfg" => {
178                return match iter.next().as_ref() {
179                    Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
180                        parse_as_empty(iter)?;
181                        Ok(Arg::Cfg(g.stream()))
182                    }
183                    tt => bail!(tt_span(tt), "expected `(`"),
184                };
185            }
186            "feature" => {
187                let next = iter.next();
188                return match next.as_ref() {
189                    Some(TokenTree::Punct(p)) if p.as_char() == '=' => match iter.next() {
190                        Some(TokenTree::Literal(l)) => {
191                            let l = LitStr::new(l)?;
192                            parse_as_empty(iter)?;
193                            Ok(Arg::Feature(
194                                vec![TokenTree::Ident(i), next.unwrap(), l.token.into()]
195                                    .into_iter()
196                                    .collect(),
197                            ))
198                        }
199                        tt => bail!(tt_span(tt.as_ref()), "expected string literal"),
200                    },
201                    tt => bail!(tt_span(tt), "expected `=`"),
202                };
203            }
204            _ => {}
205        },
206        Some(TokenTree::Literal(l)) => {
207            if let Ok(l) = LitStr::new(l) {
208                parse_as_empty(iter)?;
209                return match l.value().parse::<VersionReq>() {
210                    Ok(req) => Ok(Arg::Version(req)),
211                    Err(e) => bail!(l.span(), "{}", e),
212                };
213            }
214        }
215        Some(_) => {}
216    }
217
218    bail!(next_span, "expected one of: `nightly`, `cfg`, `feature`, string literal")
219}
220
221struct VersionReq {
222    major: u32,
223    minor: u32,
224}
225
226impl FromStr for VersionReq {
227    type Err = String;
228
229    fn from_str(s: &str) -> Result<Self, Self::Err> {
230        let mut pieces = s.split('.');
231        let major = pieces
232            .next()
233            .ok_or("need to specify the major version")?
234            .parse::<u32>()
235            .map_err(|e| e.to_string())?;
236        let minor = pieces
237            .next()
238            .ok_or("need to specify the minor version")?
239            .parse::<u32>()
240            .map_err(|e| e.to_string())?;
241        if let Some(s) = pieces.next() {
242            Err(format!("unexpected input: .{}", s))
243        } else {
244            Ok(Self { major, minor })
245        }
246    }
247}
248
249struct Version {
250    minor: u32,
251    nightly: bool,
252}
253
254// Use \ on Windows host to work around https://github.com/rust-lang/rust/issues/75075 / https://github.com/rust-lang/cargo/issues/13919.
255// (Fixed in Rust 1.84: https://github.com/rust-lang/rust/pull/125205)
256#[cfg(const_fn_has_build_script)]
257#[cfg(not(host_os = "windows"))]
258const VERSION: Version = include!(concat!(env!("OUT_DIR"), "/version"));
259#[cfg(const_fn_has_build_script)]
260#[cfg(host_os = "windows")]
261const VERSION: Version = include!(concat!(env!("OUT_DIR"), "\\version"));
262// If build script has not run or unable to determine version, it is considered as Rust 1.0.
263#[cfg(not(const_fn_has_build_script))]
264const VERSION: Version = Version { minor: 0, nightly: false };