1#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
88#![deny(missing_docs)]
89#![deny(unreachable_pub)]
90#![deny(clippy::undocumented_unsafe_blocks)]
91#![deny(clippy::multiple_unsafe_ops_per_block)]
92
93use std::borrow::Cow;
94use std::ffi::OsStr;
95use std::path::Path;
96use std::process::Command;
97
98use deps::{Dependencies, Errored};
99
100mod deps;
101mod features;
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum ExitStatus {
106 Success,
108 Failure(i32),
110}
111
112impl std::fmt::Display for ExitStatus {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 ExitStatus::Success => write!(f, "0"),
116 ExitStatus::Failure(code) => write!(f, "{code}"),
117 }
118 }
119}
120
121#[derive(Debug)]
123pub struct CompileOutput {
124 pub status: ExitStatus,
126 pub stdout: String,
129 pub stderr: String,
132}
133
134impl std::fmt::Display for CompileOutput {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 writeln!(f, "exit status: {}", self.status)?;
137 if !self.stderr.is_empty() {
138 write!(f, "--- stderr \n{}\n", self.stderr)?;
139 }
140 if !self.stdout.is_empty() {
141 write!(f, "--- stdout \n{}\n", self.stdout)?;
142 }
143 Ok(())
144 }
145}
146
147fn rustc(config: &Config, tmp_file: &Path) -> Command {
148 let mut program = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()));
149 program.env("RUSTC_BOOTSTRAP", "1");
150 let rust_flags = std::env::var_os("RUSTFLAGS");
151
152 if let Some(rust_flags) = &rust_flags {
153 program.args(
154 rust_flags
155 .as_encoded_bytes()
156 .split(|&b| b == b' ')
157 .map(|flag| unsafe { OsStr::from_encoded_bytes_unchecked(flag) }),
159 );
160 }
161
162 program.arg("--crate-name");
163 program.arg(config.function_name.split("::").last().unwrap_or("unnamed"));
164 program.arg(tmp_file);
165 program.envs(std::env::vars());
166
167 program.stderr(std::process::Stdio::piped());
168 program.stdout(std::process::Stdio::piped());
169
170 program
171}
172
173fn write_tmp_file(tokens: &str, tmp_file: &Path) {
174 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
175
176 #[cfg(feature = "prettyplease")]
177 {
178 if let Ok(syn_file) = syn::parse_file(tokens) {
179 let pretty_file = prettyplease::unparse(&syn_file);
180 std::fs::write(tmp_file, pretty_file).unwrap();
181 return;
182 }
183 }
184
185 std::fs::write(tmp_file, tokens).unwrap();
186}
187
188pub fn compile_custom(tokens: &str, config: &Config) -> Result<CompileOutput, Errored> {
190 let dependencies = Dependencies::new(config)?;
191
192 let tmp_file = Path::new(config.tmp_dir.as_ref()).join(format!("{}.rs", config.function_name.replace("::", "____")));
193 write_tmp_file(tokens, &tmp_file);
194
195 let mut program = rustc(config, &tmp_file);
196
197 dependencies.apply(&mut program);
198 program.arg("-Zunpretty=expanded");
200
201 let output = program.output().unwrap();
202
203 let stdout = String::from_utf8(output.stdout).unwrap();
204 let syn_file = syn::parse_file(&stdout);
205 #[cfg(feature = "prettyplease")]
206 let stdout = syn_file.as_ref().map(prettyplease::unparse).unwrap_or(stdout);
207
208 let mut crate_type = "lib";
209
210 if let Ok(file) = syn_file {
211 if file.items.iter().any(|item| {
212 let syn::Item::Fn(func) = item else {
213 return false;
214 };
215
216 func.sig.ident == "main"
217 }) {
218 crate_type = "bin";
219 }
220 };
221
222 let mut status = if output.status.success() {
223 ExitStatus::Success
224 } else {
225 ExitStatus::Failure(output.status.code().unwrap_or(-1))
226 };
227
228 let stderr = if status == ExitStatus::Success {
229 let mut program = rustc(config, &tmp_file);
230 dependencies.apply(&mut program);
231 program.arg("--emit=llvm-ir");
232 program.arg(format!("--crate-type={crate_type}"));
233 program.arg("-o");
234 program.arg("-");
235 let comp_output = program.output().unwrap();
236 status = if comp_output.status.success() {
237 ExitStatus::Success
238 } else {
239 ExitStatus::Failure(comp_output.status.code().unwrap_or(-1))
240 };
241 String::from_utf8(comp_output.stderr).unwrap()
242 } else {
243 String::from_utf8(output.stderr).unwrap()
244 };
245
246 let stderr = stderr.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
247 let stdout = stdout.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
248
249 Ok(CompileOutput { status, stdout, stderr })
250}
251
252#[derive(Clone, Debug)]
254pub struct Config {
255 pub manifest: Cow<'static, Path>,
259 pub target_dir: Cow<'static, Path>,
262 pub tmp_dir: Cow<'static, Path>,
264 pub function_name: Cow<'static, str>,
266 pub file_path: Cow<'static, Path>,
268 pub package_name: Cow<'static, str>,
270}
271
272#[macro_export]
273#[doc(hidden)]
274macro_rules! _function_name {
275 () => {{
276 fn f() {}
277 fn type_name_of_val<T>(_: T) -> &'static str {
278 std::any::type_name::<T>()
279 }
280 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
281 while let Some(rest) = name.strip_suffix("::{{closure}}") {
282 name = rest;
283 }
284 name
285 }};
286}
287
288#[doc(hidden)]
289pub fn build_dir() -> &'static Path {
290 Path::new(env!("OUT_DIR"))
291}
292
293#[doc(hidden)]
294pub fn target_dir() -> &'static Path {
295 build_dir()
296 .parent()
297 .unwrap()
298 .parent()
299 .unwrap()
300 .parent()
301 .unwrap()
302 .parent()
303 .unwrap()
304}
305
306#[macro_export]
307#[doc(hidden)]
308macro_rules! _config {
309 () => {{
310 $crate::Config {
311 manifest: ::std::borrow::Cow::Borrowed(::std::path::Path::new(env!("CARGO_MANIFEST_PATH"))),
312 tmp_dir: ::std::borrow::Cow::Borrowed($crate::build_dir()),
313 target_dir: ::std::borrow::Cow::Borrowed($crate::target_dir()),
314 function_name: ::std::borrow::Cow::Borrowed($crate::_function_name!()),
315 file_path: ::std::borrow::Cow::Borrowed(::std::path::Path::new(file!())),
316 package_name: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_NAME")),
317 }
318 }};
319}
320
321#[macro_export]
340macro_rules! compile {
341 ($($tokens:tt)*) => {
342 $crate::compile_str!(stringify!($($tokens)*))
343 };
344}
345
346#[macro_export]
358macro_rules! compile_str {
359 ($expr:expr) => {
360 $crate::try_compile_str!($expr).expect("failed to compile")
361 };
362}
363
364#[macro_export]
378macro_rules! try_compile {
379 ($($tokens:tt)*) => {
380 $crate::try_compile_str!(stringify!($($tokens)*))
381 };
382}
383
384#[macro_export]
391macro_rules! try_compile_str {
392 ($expr:expr) => {
393 $crate::compile_custom($expr, &$crate::_config!())
394 };
395}
396
397#[cfg(test)]
398#[cfg_attr(all(test, coverage_nightly), coverage(off))]
399mod tests {
400 use insta::assert_snapshot;
401
402 use crate::ExitStatus;
403
404 #[test]
405 fn compile_success() {
406 let out = compile! {
407 #[allow(unused)]
408 fn main() {
409 let a = 1;
410 let b = 2;
411 let c = a + b;
412 }
413 };
414
415 dbg!(&out);
416
417 assert_eq!(out.status, ExitStatus::Success);
418 assert!(out.stderr.is_empty());
419 assert_snapshot!(out);
420 }
421
422 #[test]
423 fn try_compile_success() {
424 let out = try_compile! {
425 #[allow(unused)]
426 fn main() {
427 let xd = 0xd;
428 let xdd = 0xdd;
429 let xddd = xd + xdd;
430 println!("{}", xddd);
431 }
432 };
433
434 assert!(out.is_ok());
435 let out = out.unwrap();
436 dbg!(&out);
437 assert_eq!(out.status, ExitStatus::Success);
438 assert!(out.stderr.is_empty());
439 assert!(!out.stdout.is_empty());
440 }
441
442 #[test]
443 fn compile_failure() {
444 let out = compile! {
445 invalid_rust_code
446 };
447
448 assert_eq!(out.status, ExitStatus::Failure(1));
449 assert!(out.stdout.is_empty());
450 assert_snapshot!(out);
451 }
452
453 #[test]
454 fn try_compile_failure() {
455 let out = try_compile! {
456 invalid rust code
457 };
458
459 assert!(out.is_ok());
460 let out = out.unwrap();
461 assert_eq!(out.status, ExitStatus::Failure(1));
462 assert!(out.stdout.is_empty());
463 assert!(!out.stderr.is_empty());
464 }
465}