//! Compile-time expression evaluation.
//! This crate is inspired by [Zig's `comptime`](https://ziglang.org/documentation/master/#comptime).
//!
//! The passed closure will be evaluated at compile time.
//!
//! ### Example
//!
//! ```
//! println!(
//! "The program was compiled on {}.",
//! // note how chrono::Utc is transported
//! edg::r! { || -> chrono::DateTime<chrono::Utc> { chrono::Utc::now() } }.format("%Y-%m-%d").to_string()
//! ); // The program was compiled on 2023-11-16.
//! ```
//!
//! ### Limitations
//!
//! - Unlike Zig, `edg::r!` does not have access to the scope in which it is invoked, as
//! the closure in `edg::r!` is run as its own script.
//! - Unfortunately, as `serde` is not const, you cant have `const X: _ = edg::r! { .. }`.
//! - Each block must be compiled sequentially.
//!
//! ### How it works
//!
//! `edg::r!`:
//!
//! - adds serde_json::to_string to your code
//! - creates a file `edg-{hash}.rs`, with your new code, in your target directory
//! - compiles the file with `rustc`
//! - executes the file
//! - emits code to deserialize the json output.
//!
//! #### Predecessor
//!
//! Much of the code is from the [`comptime`](https://crates.io/crates/comptime) crate.
extern crate proc_macro;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
process::Command,
};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{ExprClosure, ReturnType};
#[proc_macro]
/// Run a closure at compile time.
/// This closure is completely isolated.
/// You may return any data structure that implements [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) and [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html).
///
/// ```
/// let rand = edg::r! { || -> i32 {
/// # mod rand { pub fn random() -> i32 { 4 } }
/// rand::random()
/// } };
/// ```
pub fn r(input: TokenStream) -> TokenStream {
let out_dir = std::env::current_dir().map_or("/tmp".into(), |p| {
p.join("target")
.exists()
.then(|| p.join("target"))
.unwrap_or(p)
});
macro_rules! err {
($fstr:literal$(,)? $( $arg:expr ),*) => {{
let compile_error = format!($fstr, $($arg),*);
return TokenStream::from(quote!(compile_error!(#compile_error)));
}};
}
// lock(&out_dir);
let args: Vec<_> = std::env::args().collect();
let input = syn::parse_macro_input!(input as ExprClosure);
let ty = match input.output {
ReturnType::Default => err!("specify return type of closure"),
ReturnType::Type(_, t) => t,
};
let code = input.body.to_token_stream().to_string();
let mut hasher = DefaultHasher::new();
code.hash(&mut hasher);
let hash = hasher.finish();
let file = out_dir.join(format!("edg-{hash}.rs"));
std::fs::write(
&file,
format!(
r#"fn main() {{
let res: {} =
{code}
; // surely nobody will main()
let ser = serde_json::to_string(&res).expect("serialization failed");
print!("{{ser}}");
}}"#,
ty.to_token_stream()
),
)
.expect("could not write file");
let out = format!("edg_{hash}");
let mut rustc = Command::new("rustc");
rustc.args(filter_rustc_args(&args));
rustc.args(["--crate-name", &*out]);
rustc.args(["--crate-type", "bin"]);
rustc.args(["--out-dir".as_ref(), out_dir.as_os_str()]);
rustc.args(merge_externs(&args));
rustc.arg(file.to_str().unwrap());
let compile_output = rustc.output().expect("could not invoke rustc");
if !compile_output.status.success() {
err!(
"could not compile comptime expr:\n\n{}\n",
String::from_utf8(compile_output.stderr).unwrap()
);
}
print!("{}", String::from_utf8(compile_output.stdout).unwrap());
print!("{}", String::from_utf8(compile_output.stderr).unwrap());
let extra = args
.iter()
.find(|a| a.starts_with("extra-filename="))
.map(|ef| ef.split('=').nth(1).unwrap())
.unwrap_or_default();
let out = out_dir.join(format!("{out}{extra}"));
let comptime_output = Command::new(&out)
.output()
.expect("could not invoke edg_bin");
if !comptime_output.status.success() {
err!(
"could not run comptime expr:\n\n{}\n",
String::from_utf8(comptime_output.stderr).unwrap()
);
}
let comptime_expr = if let Ok(output) = String::from_utf8(comptime_output.stdout) {
output
} else {
err!("comptime expr output was not utf8")
};
_ = std::fs::remove_file(file);
_ = std::fs::remove_file(out);
// unlock(&out_dir);
quote!(::serde_json::from_str::<#ty>(#comptime_expr).expect(&format!("deser of expr ({}) failed (bug in `Deserialize` impl)", #comptime_expr))).into()
}
fn filter_rustc_args(args: &[String]) -> Vec<&str> {
let mut rustc_args = Vec::with_capacity(args.len());
let mut skip = true;
for arg in args {
if &**arg == "-" {
continue;
}
if skip {
skip = false;
continue;
}
if arg == "--crate-type" || arg == "--crate-name" || arg == "--extern" || arg == "--out-dir" || arg == "-o" {
skip = true;
} else if arg.ends_with(".rs")
|| arg == "--test"
|| arg == "rustc"
|| arg.starts_with("--emit")
{
continue;
} else {
rustc_args.push(&**arg);
}
}
rustc_args
}
fn merge_externs(args: &[String]) -> Vec<&str> {
let mut found = false;
let mut ret = vec![];
for arg in args {
match &**arg {
arg if found => {
found = false;
ret.push("--extern");
ret.push(arg);
}
"--extern" => found = true,
_ => continue,
}
}
ret
}