├── .gitignore ├── .rustfmt.toml ├── examples ├── Cargo.toml └── src │ └── main.rs ├── Cargo.toml ├── .github └── workflows │ └── main.yml ├── tests └── integration.rs ├── LICENSE ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_strings = true 2 | imports_granularity = "Crate" 3 | group_imports = "StdExternalCrate" 4 | edition = "2021" 5 | version = "One" 6 | unstable_features = true 7 | error_on_unformatted = true 8 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "comptime-tests" 3 | version = "0.1.1" 4 | authors = ["Nick Hynes "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | comptime = { path = "../" } 9 | rand = "0.7" 10 | chrono = "0.4" 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "comptime" 3 | version = "1.0.0" 4 | authors = ["Nick Hynes ", "Zoe Soutter "] 5 | edition = "2021" 6 | description = "Compile-time code execution (i.e. lightweight proc-macro)" 7 | readme = "README.md" 8 | repository = "https://github.com/nhynes/comptime-rs" 9 | license = "MIT" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | proc-macro2 = "1.0" 16 | quote = "1.0" 17 | syn = { version = "1.0", features = ["full"] } 18 | 19 | [dev-dependencies] 20 | rand = "0.7" 21 | chrono = "0.4" 22 | -------------------------------------------------------------------------------- /examples/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!(concat!( 3 | "The program was compiled on ", 4 | comptime::comptime! { 5 | chrono::Utc::now().format("%Y-%m-%d").to_string() 6 | }, 7 | "." 8 | )); 9 | // attribute macro comptime functions cannot be used as "literals" 10 | // as there is still a function call being made, even if though it 11 | // just immediately returns an `&'static str`. 12 | println!("This program was compiled on {}", test()); 13 | } 14 | 15 | #[comptime::comptime_fn] 16 | fn test() -> &'static str { 17 | chrono::Utc::now().format("%Y-%m-%d").to_string() 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: Use real (nightly) Rust 13 | run: | 14 | rustup default nightly-2019-08-26 # known to have rustfmt and clippy 15 | rustup component add rustfmt clippy 16 | 17 | - name: Checkstyle 18 | run: | 19 | cargo clippy 20 | cargo fmt -- --check 21 | 22 | - name: Checkstyle Tests 23 | working-directory: tests 24 | run: | 25 | cargo build # @see `comptime!` limitations 26 | cargo clippy --tests 27 | cargo fmt -- --check 28 | 29 | - name: Test 30 | working-directory: tests 31 | run: cargo test 32 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | #[macro_use] 4 | extern crate comptime; 5 | 6 | #[test] 7 | fn test_attribute() { 8 | assert_eq!("5 + 6 = 11", at_comptime()) 9 | } 10 | #[comptime::comptime_fn] 11 | fn at_comptime() -> &'static str { 12 | format!("5 + 6 = {}", 5 + 6) 13 | } 14 | #[test] 15 | fn test_basic() { 16 | assert_eq!( 17 | concat!("u32 is ", comptime!(std::mem::size_of::()), " bytes"), 18 | "u32 is 4 bytes" 19 | ); 20 | } 21 | 22 | #[test] 23 | fn test_inner_mac() { 24 | assert_eq!(comptime!(stringify!(4)), "4"); 25 | } 26 | 27 | #[test] 28 | fn test_inner_crate() { 29 | assert_eq!( 30 | comptime! { 31 | use rand::{SeedableRng, RngCore}; 32 | rand::rngs::StdRng::seed_from_u64(42u64).next_u64() 33 | }, 34 | 9_482_535_800_248_027_256u64 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Nick Hynes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightweight compile-time expression evaluation. 2 | This crate is inspired by [Zig's `comptime`](https://ziglang.org/documentation/master/#comptime). 3 | 4 | The expression returned by the contents of the comptime macro invocation will be parsed as 5 | Rust source code and inserted back into the call site. 6 | 7 | **tl;dr:** `comptime!` gives you no-context anonynmous proc macros. 8 | 9 | ### Example 10 | 11 | ## proc-macro 12 | 13 | ```rust 14 | #![feature(proc_macro_hygiene)] 15 | fn main() { 16 | println!(concat!( 17 | "The program was compiled on ", 18 | comptime::comptime! { 19 | chrono::Utc::now().format("%Y-%m-%d").to_string() 20 | }, 21 | "." 22 | )); // The program was compiled on 2024-05-22. 23 | } 24 | ``` 25 | 26 | ## Attribute macro 27 | 28 | ```rust 29 | fn main() { 30 | println!("{}", at_comptime()); // The program was compiled on 2024-05-22. 31 | } 32 | #[comptime::comptime_fn] 33 | fn at_comptime() -> &'static str { 34 | format!(concat!( 35 | "The program was compiled on ", 36 | comptime::comptime! { 37 | chrono::Utc::now().format("%Y-%m-%d").to_string() 38 | }, 39 | "." 40 | )) 41 | } 42 | ``` 43 | 44 | ### Limitations 45 | 46 | Unlike the real `comptime`, `comptime!` does not have access to the scope in which it is invoked. 47 | The code in `comptime!` is run as its own script. 48 | Though, technically, you could interpolate static values using `quote!`. 49 | 50 | Also, `comptime!` requires you to run `cargo build` at least once before `cargo (clippy|check)` 51 | will work since `comptime!` does not compile dependencies. 52 | 53 | Strings generated with comptime_fn will be represented as `&'static str` as it is known at compile time, to fix this, you need to simply run `String::from(comptime_fn())` or `comptime_fn().to_string()`. 54 | 55 | Due to how the comptime macro works, writing to stdout will almost definitely cause comptime-rs to fail to build. 56 | 57 | The comptime_fn attribute macro still makes the function call to the compile time function, but all calculations inside that function are performed at compile time. e.g. 58 | 59 | ```rust 60 | #[comptime::comptime_fn] 61 | fn costly_calculation() -> i32 { 62 | 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 // Any calculations 63 | } 64 | ``` 65 | 66 | will be turned into 67 | 68 | ```rust 69 | #[comptime::comptime_fn] 70 | fn costly_calculation() -> i32 { 71 | 362880 72 | } 73 | ``` 74 | 75 | ### Contributing 76 | 77 | Please do! 78 | Ideally, `rustc` would also have (real) `comptime` which would have access to type information and other static values. 79 | In the meantime, this should be a nice way to approximate and experiment with such functionality. 80 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Lightweight compile-time expression evaluation. 2 | //! This crate is inspired by [Zig's `comptime`](https://ziglang.org/documentation/master/#comptime). 3 | //! 4 | //! The expression returned by the contents of the comptime macro invocation will be parsed as 5 | //! Rust source code and inserted back into the call site. 6 | //! 7 | //! **tl;dr:** `comptime!` gives you no-context anonynmous proc macros. 8 | //! 9 | //! ### Example 10 | //! 11 | //! ``` compile_fail 12 | //! println!(concat!( 13 | //! "The program was compiled on ", 14 | //! comptime::comptime! { 15 | //! chrono::Utc::now().format("%Y-%m-%d").to_string() 16 | //! }, 17 | //! "." 18 | //! )); // The program was compiled on 2019-08-30. 19 | //! ``` 20 | //! 21 | //! ### Limitations 22 | //! 23 | //! Unlike Zig, `comptime!` does not have access to the scope in which it is invoked. 24 | //! The code in `comptime!` is run as its own script. Though, technically, you could 25 | //! interpolate static values using `quote!`. 26 | //! 27 | //! Also, `comptime!` requires you to run `cargo build` at least once before `cargo (clippy|check)` 28 | //! will work since `comptime!` does not compile dependencies. 29 | //! 30 | //! Finally, using this macro in doctests may fail with strange errors for no good reason. This is 31 | //! because output directory detection is imperfect and sometimes breaks. You have been warned. 32 | 33 | extern crate proc_macro; 34 | 35 | use std::{ 36 | collections::{ 37 | hash_map::{DefaultHasher, Entry}, 38 | HashMap, 39 | }, 40 | hash::{Hash, Hasher}, 41 | path::Path, 42 | process::Command, 43 | }; 44 | 45 | use proc_macro::TokenStream; 46 | use quote::{quote, ToTokens, TokenStreamExt}; 47 | use syn::{ 48 | parse::{Parse, ParseStream}, 49 | ItemFn, 50 | }; 51 | 52 | macro_rules! err { 53 | ($fstr:literal$(,)? $( $arg:expr ),*) => {{ 54 | let compile_error = format!($fstr, $($arg),*); 55 | return TokenStream::from(quote!(compile_error!(#compile_error))); 56 | }}; 57 | } 58 | 59 | struct BlockInner { 60 | stmts: Vec, 61 | } 62 | 63 | impl Parse for BlockInner { 64 | fn parse(input: ParseStream) -> syn::Result { 65 | Ok(Self { 66 | stmts: syn::Block::parse_within(input)?, 67 | }) 68 | } 69 | } 70 | 71 | impl ToTokens for BlockInner { 72 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 73 | tokens.append_all(self.stmts.iter()); 74 | } 75 | } 76 | 77 | #[proc_macro_attribute] 78 | pub fn comptime_fn(_args: TokenStream, item: TokenStream) -> TokenStream { 79 | let input = syn::parse_macro_input!(item as ItemFn); 80 | 81 | let ItemFn { 82 | // The function signature 83 | sig, 84 | // The visibility specifier of this function 85 | vis, 86 | // The function block or body 87 | block, 88 | // Other attributes applied to this function 89 | attrs, 90 | } = input; 91 | let result: proc_macro2::TokenStream = comptime(block.to_token_stream().into()).into(); 92 | quote::quote!( 93 | #(#attrs)* 94 | #vis #sig { 95 | #result 96 | } 97 | ) 98 | .into() 99 | } 100 | 101 | #[proc_macro] 102 | pub fn comptime(input: TokenStream) -> TokenStream { 103 | let args: Vec<_> = std::env::args().collect(); 104 | let get_arg = |arg| { 105 | args.iter() 106 | .position(|a| a == arg) 107 | .and_then(|p| args.get(p + 1)) 108 | }; 109 | 110 | let comptime_program = syn::parse_macro_input!(input as BlockInner); 111 | 112 | let out_dir = match get_arg("--out-dir") { 113 | Some(out_dir) => Path::new(out_dir), 114 | None => { 115 | err!("comptime failed: could not determine rustc out dir."); 116 | } 117 | }; 118 | 119 | let comptime_program_str = comptime_program.to_token_stream().to_string(); 120 | let mut hasher = DefaultHasher::new(); 121 | comptime_program_str.hash(&mut hasher); 122 | let comptime_disambiguator = hasher.finish(); 123 | 124 | let comptime_rs = out_dir.join(format!("comptime-{}.rs", comptime_disambiguator)); 125 | std::fs::write( 126 | &comptime_rs, 127 | format!( 128 | r#"fn main() {{ 129 | let comptime_output = {{ {} }}; 130 | print!("{{}}", quote::quote!(#comptime_output)); 131 | }}"#, 132 | comptime_program_str 133 | ), 134 | ) 135 | .expect("could not write comptime.rs"); 136 | Command::new("rustfmt").arg(&comptime_rs).output().ok(); 137 | 138 | let mut rustc_args = filter_rustc_args(&args); 139 | rustc_args.push("--crate-name".to_string()); 140 | rustc_args.push("comptime_bin".to_string()); 141 | rustc_args.push("--crate-type".to_string()); 142 | rustc_args.push("bin".to_string()); 143 | rustc_args.push("--emit=dep-info,link".to_string()); 144 | rustc_args.append(&mut merge_externs(out_dir, &args)); 145 | rustc_args.push(comptime_rs.to_str().unwrap().to_string()); 146 | 147 | let compile_output = Command::new("rustc") 148 | .args(&rustc_args) 149 | .output() 150 | .expect("could not invoke rustc"); 151 | if !compile_output.status.success() { 152 | err!( 153 | "could not compile comptime expr:\n\n{}\n", 154 | String::from_utf8(compile_output.stderr).unwrap() 155 | ); 156 | } 157 | 158 | let extra_filename = args 159 | .iter() 160 | .find(|a| a.starts_with("extra-filename=")) 161 | .map(|ef| ef.split('=').nth(1).unwrap()) 162 | .unwrap_or_default(); 163 | let comptime_bin = out_dir.join(format!("comptime_bin{}", extra_filename)); 164 | 165 | let comptime_output = Command::new(&comptime_bin) 166 | .output() 167 | .expect("could not invoke comptime_bin"); 168 | 169 | if !comptime_output.status.success() { 170 | err!( 171 | "could not run comptime expr:\n\n{}\n", 172 | String::from_utf8(comptime_output.stderr).unwrap() 173 | ); 174 | } 175 | 176 | let comptime_expr_str = match String::from_utf8(comptime_output.stdout) { 177 | Ok(output) => output, 178 | Err(_) => err!("comptime expr output was not utf8"), 179 | }; 180 | let comptime_expr: syn::Expr = match syn::parse_str(&comptime_expr_str) { 181 | Ok(expr) => expr, 182 | Err(_) => syn::ExprLit { 183 | attrs: Vec::new(), 184 | lit: syn::LitStr::new(&comptime_expr_str, proc_macro2::Span::call_site()).into(), 185 | } 186 | .into(), 187 | }; 188 | 189 | std::fs::remove_file(comptime_rs).ok(); 190 | std::fs::remove_file(comptime_bin).ok(); 191 | 192 | TokenStream::from(comptime_expr.to_token_stream()) 193 | } 194 | 195 | /// Returns the rustc args needed to build the comptime executable. 196 | fn filter_rustc_args(args: &[String]) -> Vec { 197 | let mut rustc_args = Vec::with_capacity(args.len()); 198 | let mut skip = true; // skip the invoked program 199 | for arg in args { 200 | if skip { 201 | skip = false; 202 | continue; 203 | } 204 | if arg == "--crate-type" || arg == "--crate-name" || arg == "--extern" { 205 | skip = true; 206 | } else if arg.ends_with(".rs") 207 | || arg == "--test" 208 | || arg == "rustc" 209 | || arg.starts_with("--emit") 210 | { 211 | continue; 212 | } else { 213 | rustc_args.push(arg.clone()); 214 | } 215 | } 216 | rustc_args 217 | } 218 | 219 | fn merge_externs(deps_dir: &Path, args: &[String]) -> Vec { 220 | let mut cargo_rlibs = HashMap::new(); // libfoo -> /path/to/libfoo-12345.rlib 221 | let mut next_is_extern = false; 222 | for arg in args { 223 | if next_is_extern { 224 | let mut libname_path = arg.split('='); 225 | let lib_name = libname_path.next().unwrap(); // libfoo 226 | let path = Path::new(libname_path.next().unwrap()); 227 | if path.extension().unwrap() == "rlib" { 228 | cargo_rlibs.insert(lib_name.to_string(), path.to_path_buf()); 229 | } 230 | } 231 | next_is_extern = arg == "--extern"; 232 | } 233 | 234 | let mut dep_dirents: Vec<_> = std::fs::read_dir(deps_dir) 235 | .unwrap() 236 | .filter_map(|de| { 237 | let de = de.unwrap(); 238 | let p = de.path(); 239 | let fname = p.file_name().unwrap().to_str().unwrap(); 240 | if fname.starts_with("lib") && fname.ends_with(".rlib") { 241 | Some(de) 242 | } else { 243 | None 244 | } 245 | }) 246 | .collect(); 247 | dep_dirents.sort_by_key(|de| std::cmp::Reverse(de.metadata().and_then(|m| m.created()).ok())); 248 | 249 | for dirent in dep_dirents { 250 | let path = dirent.path(); 251 | let fname = path.file_name().unwrap().to_str().unwrap(); 252 | if !fname.ends_with(".rlib") { 253 | continue; 254 | } 255 | let lib_name = fname.rsplit_once('-').unwrap().0.to_string(); 256 | // ^ reverse "libfoo-disambiguator" then split off the disambiguator 257 | if let Entry::Vacant(ve) = cargo_rlibs.entry(lib_name) { 258 | ve.insert(path); 259 | } 260 | } 261 | 262 | let mut merged_externs = Vec::with_capacity(cargo_rlibs.len() * 2); 263 | for (lib_name, path) in cargo_rlibs.iter() { 264 | merged_externs.push("--extern".to_string()); 265 | merged_externs.push(format!( 266 | "{}={}", 267 | &lib_name.strip_prefix("lib").unwrap_or(lib_name), 268 | path.display() 269 | )); 270 | } 271 | 272 | merged_externs 273 | } 274 | --------------------------------------------------------------------------------