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