diff --git a/.github/workflows/rust.yml b/.github/workflows/cargo-test.yml similarity index 83% rename from .github/workflows/rust.yml rename to .github/workflows/cargo-test.yml index 715229f2..78a0cd82 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/cargo-test.yml @@ -1,4 +1,4 @@ -name: Build + Test +name: rustyrts - Test on: push: @@ -25,12 +25,16 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2023-01-20 + toolchain: nightly-2023-12-28 override: true - name: Install Rust components run: rustup component add rustc-dev llvm-tools-preview - name: Check Rust version run: cargo --version && rustc --version + + - name: Rust Cache + uses: Swatinem/rust-cache@v2.7.0 + - name: Build run: cargo build --verbose - name: Run tests diff --git a/.helix/config.toml b/.helix/config.toml new file mode 100644 index 00000000..eb6a4e86 --- /dev/null +++ b/.helix/config.toml @@ -0,0 +1 @@ +editor.workspace-lsp-roots = ["rustyrts-dynamic-rlib", "rustyrts-dynamic-runner"] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 284037a6..4b1202a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,9 @@ edition = "2021" [package.metadata.rust-analyzer] rustc_private=true +[profile.release] +debug = true + [[bin]] name = "cargo-rustyrts" @@ -15,12 +18,6 @@ name = "rustyrts-static" [[bin]] name = "rustyrts-dynamic" -[features] -default = ["print_paths"] - -print_paths = [] -monomorphize = [] - [dependencies] log = "0.4" serde_json = "1.0.61" @@ -31,4 +28,9 @@ env_logger = "0.10.0" regex = "1.7.1" lazy_static = "1.4.0" once_cell = "1.17.1" -threadpool = "1.8.1" \ No newline at end of file +threadpool = "1.8.1" +bimap = "0.6.3" + +[dev-dependencies] +tempdir = "0.3.7" +test-case = "3.1.0" diff --git a/README.md b/README.md index e1038e57..770097dd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ -# `rustyRTS` +# RustyRTS -`rustyRTS` is a regression test selection tool for Rust projects. +RustyRTS is a regression test selection tool for Rust projects. +It provides two ways of selecting tests: -# Prerequisites -- developed on Rust nightly-2023-01-20 - in other versions the API of rustc_driver may differ slightly +- `cargo rustyrts dynamic` instruments all binaries to trace which functions are executed during the tests + - ${\color{lightgreen}+++}$ extremely precise + - ${\color{lightgreen}+}$ can trace child processes (linux only) + - ${\color{red}-}$ tampers with binaries (not always desired) + - ${\color{red}-}$ needs to isolate tests in separate processes if tests are executed in parallel (not always feasible) + - ${\color{red}-}$ needs to execute test sequentially/single-threaded on Windows + - ${\color{orange}/}$ small compilation overhead, moderate runtime overhead -# Setup -To build `rustyRTS` simply run: -``` -$ cargo install --path . -``` -This will build the required executables and install them to your local cargo directory. +- `cargo rustyrts static` creates a directed dependency graph via static analysis + - ${\color{lightgreen}+}$ quite precise + - ${\color{lightgreen}+}$ does not tamper with binaries at all + - ${\color{lightgreen}+}$ no runtime overhead + - ${\color{orange}/}$ moderate compilation overhead + +Whenever it detects that some test depends on a function that has changed, this test is selected. + +# Rust version +RustyRTS depends on the internals of the `rustc` compiler, which are quite unstable. +It has been developed for *v1.77.0-nightly* and can currently only be used with this specific toolchain version. ## Setup Rust toolchain The correct toolchain should be installed automatically when building `rustyRTS`. @@ -20,6 +31,13 @@ $ rustup default nightly-2023-01-20 # (recommended: to use this toolchain by def $ rustup override set nightly-2023-01-20 # (to use this toolchain in current directory only) ``` +# How to install +To install RustyRTS simply run: +``` +$ cargo install --path . +``` +This will first install the required toolchain, if it is not present, and then build the required executables and install them to your local cargo directory. + # Usage | Command | Explanation | | -------- | ----------- | @@ -29,70 +47,8 @@ $ rustup override set nightly-2023-01-20 # (to use this toolchain in current dir ## Custom arguments to `rustc`, `cargo build` or `cargo test` `cargo rustyrts [dynamic|static] -- -- -- ` +(We are planning to make this more ergonomic soon...) For example: `cargo rustyrts dynamic -- -- --emit=mir` - to generate a human-readable representation of the MIR `cargo rustyrts dynamic -- -- -- -- --test-threads 1` - to execute test single-threaded without forking - -# How tests are selected - -## Checksums -RustyRTS (both static and dynamic) keeps track of modifications to the code by calculating and comparing checksums of MIR [`Body`s](https://doc.rust-lang.org/stable/nightly-rustc/rustc_middle/mir/struct.Body.html), which correspond to functions. When the checksum of a `Body` differs between old and new revision, it is considered changed. - -Furthermore, the checksums of [`ConstAllocation`s](https://doc.rust-lang.org/stable/nightly-rustc/rustc_middle/mir/interpret/allocation/struct.ConstAllocation.html) corresponding to `static var` or `static mut var` are contributing to the checksum of every `Body` that accesses the respective variable. This enables dynamic RustyRTS to recognize changes in compile-time evaluation, where instrumentation for tracing is not possible. In static RustyRTS this allows to restrict the analysis to functions that are relevant at runtime (i.e. not only used in compile-time evaluation). - -Lastly, the checksums of [`VtblEntry`s](https://doc.rust-lang.org/stable/nightly-rustc/rustc_middle/ty/vtable/enum.VtblEntry.html) (vtable entries) are contributing to the checksum of the function that they are pointing to. -Assume a vtable entry that was pointing to a function a) in the old revision is now pointing to a different function b) in the new revision. -Because static RustyRTS is working entirely on the graph data of the new revision, it is sufficient to consider function b) changed, as long as there is a continuous path from a corresponding test to function b). -Dynamic RustyRTS is comparing traces originating from the old revision, which is why function a) would be considered changed. -Because static RustyRTS can distinguish whether a function is called via dynamic or static dispatch, these additional checksums of vtable entries only contribute in the case of dynamic dispatch. - -## Dynamic -Dynamic RustyRTS collects traces containing the names of all functions that are called during the execution of a test. Some helper functions and global variables are used to obtain those traces: -- a `static HashSet<(&'static str, ..)>` for collecting names of traced functions -- `trace(input: &'static str, ..)` is used to append `input` to the hash set -- `pre_test()` which initializes the hash set -- `post_test(test_name: &str)` which writes the content of the hash set, i.e. the traces to a file identified by the name of the test, where the traces can be inspected in the subsequent run - -Further, on unix-like systems only: -- `pre_main()` which initializes the hash set, in case this has not already been done -- `post_main()` which appends the content of the hash set to a file identified by the `ppid` of the currently running process -- in both `post_test(test_name: &str)` and `post_main()` traces in files identified by the `pid` of the process (i.e. the `ppid` of any forked process), are appended to the hash set before exporting the traces - -During compilation, the MIR is modified, automatically generating MIR code that does not reflect in source code. Dynamic RustyRTS injects function calls into certain MIR `Body`s: -- a call to `trace()` at the beginning of every MIR `Body` -- a call to `pre_test()` at the beginning of every test function -- a call to `post_test()` at the end of every test function - -On unix-like systems only: -- a call to `pre_main()` at the beginning of every main function -- a call to `post_main()` at the end of every main function - -Calls to `post_test(test_name: &str)` and `post_main()` are injected in such a way, that as long as the process terminates gracefully (i.e. either by exiting or by unwinding) the traces are written to the filesystem. A process crashing will result in the traces not being written! - -Warning: `trace(<>)` internally uses allocations and locks, such that using custom allocators or signals may lead to a deadlock because of non-reentrant locks. (Using reentrant locks would lead to a stack overflow, which is equally bad.) - -On unix-like systems, a special test runner is used to fork for every test case, thus isolating the tests in their own process. -Forking ensures that traces do not intermix, when executing tests in parallel. When executing tests sequentially, forking is not necessary and can be omitted. - -During the subsequent run, the traces are compared to the set of changed `Body`s. If these two sets overlap, the corresponding test is considered affected. - -## Static -Static RustyRTS analyzes the MIR during compilation, without modifying it, to build a (directed) dependency graph. Edges are created according to the following criteria: -1. `EdgeType::Closure`: function -> contained Closure -2. `EdgeType::Generator`: function -> contained Generator -3. 1. `EdgeType::FnDefTrait`: caller function -> callee `fn` (for assoc `fn`s in `trait {..}`) -3. 3. `EdgeType::FnDefImpl`: caller function -> callee `fn` (for assoc `fn`s in `impl .. {..}`) -3. 3. `EdgeType::FnDef`: caller function -> callee `fn` (for non-assoc `fn`s, i.e. not inside `impl .. {..}`) -3. 4. `EdgeType::FnDefDyn`: caller function -> callee `fn` + !dyn (for functions in `trait {..} called by dynamic dispatch) -4. `EdgeType::TraitImpl`: function in `trait` definition + !dyn -> function in trait impl (`impl for ..`) + !dyn -5. `EdgeType::DynFn`: (only for associated functions) function + !dyn -> function -6. `EdgeType::Drop`: function -> destructor (`drop()` function) of referenced abstract datatype - -The suffix "!dyn" is used to distinguish static and dynamic dispatch. Checksums from vtable entries only contribute to the function they are pointing to with suffix !dyn. - -All these functions are not yet monomorphized. Using names of fully monomorphized functions may increase precision, but turns out to be impractical. On larger projects, it would bloat up the graph, such that reading the graph takes a long time. Moreover, RustyRTS compares checksums of non-monomorphized functions. -It is nevertheless possible to use fully monomorphized function names using the `monomorphize` feature. - - -When there is a path from a test to a changed `Body`, the test is considered affected. \ No newline at end of file diff --git a/build.rs b/build.rs index d4ea0776..a125adcb 100644 --- a/build.rs +++ b/build.rs @@ -8,11 +8,13 @@ fn cargo() -> Command { } fn main() { - build_library("rustyrts-dynamic-rlib"); - build_library("rustyrts-dynamic-runner"); + if std::env::var("RUSTYRTS_SKIP_BUILD").is_err() { + build_library("rustyrts-dynamic-rlib"); + build_library("rustyrts-dynamic-runner"); - install_rlib("rustyrts_dynamic_rlib", "rustyrts-dynamic-rlib"); - install_staticlib("rustyrts_dynamic_runner", "rustyrts-dynamic-runner"); + install_rlib("rustyrts_dynamic_rlib", "rustyrts-dynamic-rlib"); + install_staticlib("rustyrts_dynamic_runner", "rustyrts-dynamic-runner"); + } } fn build_library(dir_name: &str) { @@ -42,6 +44,7 @@ fn build_library(dir_name: &str) { path.push(dir_name); cmd.current_dir(path); cmd.arg("build"); + cmd.arg("--release"); match cmd.status() { Ok(exit) => { @@ -64,7 +67,7 @@ fn install_rlib(name: &str, dir_name: &str) { path.push(dir); path.push(dir_name); path.push("target"); - path.push("debug"); + path.push("release"); //path.push("deps"); let files: Vec = read_dir(path) @@ -77,19 +80,7 @@ fn install_rlib(name: &str, dir_name: &str) { //let rmeta_file = find_file(&format!("lib{}", name), ".rmeta", &files); let d_file = find_file(name, ".d", &files); - let mut cargo_home = { - let maybe_cargo_home = std::env::var("CARGO_HOME"); - if let Ok(cargo_home) = maybe_cargo_home { - PathBuf::from(cargo_home) - } else { - let home = std::env::var("HOME").expect("Unable to find HOME environment variable"); - let mut path = PathBuf::new(); - path.push(home); - path.push(".cargo"); - path - } - }; - cargo_home.push("bin"); + let cargo_home = get_cargo_home(); if let Some(entry) = rlib_file { let src = entry.path(); @@ -119,7 +110,7 @@ fn install_staticlib(name: &str, dir_name: &str) { let mut dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); dir.push(dir_name); dir.push("target"); - dir.push("debug"); + dir.push("release"); //dir.push("deps"); let files: Vec = read_dir(dir) @@ -131,19 +122,7 @@ fn install_staticlib(name: &str, dir_name: &str) { let a_file = find_file(&format!("lib{}", name), ".a", &files); let d_file = find_file(name, ".d", &files); - let mut cargo_home = { - let maybe_cargo_home = std::env::var("CARGO_HOME"); - if let Ok(cargo_home) = maybe_cargo_home { - PathBuf::from(cargo_home) - } else { - let home = std::env::var("HOME").expect("Unable to find HOME environment variable"); - let mut path = PathBuf::new(); - path.push(home); - path.push(".cargo"); - path - } - }; - cargo_home.push("bin"); + let cargo_home = get_cargo_home(); if let Some(entry) = a_file { let src = entry.path(); @@ -160,6 +139,23 @@ fn install_staticlib(name: &str, dir_name: &str) { } } +fn get_cargo_home() -> PathBuf { + let mut cargo_home = { + let maybe_cargo_home = std::env::var("CARGO_HOME"); + if let Ok(cargo_home) = maybe_cargo_home { + PathBuf::from(cargo_home) + } else { + let home = std::env::var("HOME").expect("Unable to find HOME environment variable"); + let mut path = PathBuf::new(); + path.push(home); + path.push(".cargo"); + path + } + }; + cargo_home.push("bin"); + cargo_home +} + fn find_file<'a>( starts_with: &str, ends_with: &str, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ab5b6ead..4aa72e64 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2023-01-20" -components = [ "rustc-dev", "llvm-tools-preview" ] +channel = "nightly-2023-12-28" +components = [ "rustc-dev", "rust-src", "llvm-tools"] diff --git a/rustyrts-dynamic-rlib/rust-toolchain.toml b/rustyrts-dynamic-rlib/rust-toolchain.toml new file mode 120000 index 00000000..43fe0ada --- /dev/null +++ b/rustyrts-dynamic-rlib/rust-toolchain.toml @@ -0,0 +1 @@ +../rust-toolchain.toml \ No newline at end of file diff --git a/rustyrts-dynamic-rlib/src/lib.rs b/rustyrts-dynamic-rlib/src/lib.rs index 672cfc33..37e46a62 100644 --- a/rustyrts-dynamic-rlib/src/lib.rs +++ b/rustyrts-dynamic-rlib/src/lib.rs @@ -1,84 +1,67 @@ use fs_utils::{get_dynamic_path, get_process_traces_path, get_traces_path, write_to_file}; -use std::hash::Hash; +use std::borrow::Cow; use std::path::PathBuf; -use std::sync::Mutex; +use std::sync::atomic::AtomicPtr; +use std::sync::atomic::Ordering; use std::{collections::HashSet, fs::read_to_string}; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::{AcqRel, Acquire, Release}; - mod constants; mod fs_utils; -static NODES: Mutex>> = Mutex::new(None); +static LIST: AtomicPtr = AtomicPtr::new(std::ptr::null::() as *mut Traced); //###################################################################################################################### -// Newtype tuple to specify Hash, PartialEq and Eq - -struct Traced(&'static str, &'static u8); - -impl Hash for Traced { - fn hash(&self, state: &mut H) { - (self.1 as *const u8 as usize).hash(state); - } -} - -impl PartialEq for Traced { - fn eq(&self, other: &Self) -> bool { - self.1 as *const u8 as usize == other.1 as *const u8 as usize - } -} +// Tuple type for tracing -impl Eq for Traced {} +struct Traced(&'static str, AtomicPtr); -//###################################################################################################################### +//##########>############################################################################################################ // Functions for tracing #[no_mangle] -pub fn trace(input: &'static str, bit: &'static u8) { - // SAFETY: We are given a reference to a u8 which has the same memory representation as bool, - // and therefore also AtomicBool. - let flag: &'static AtomicBool = unsafe { std::mem::transmute(bit) }; - - if !flag.load(Acquire) { - if !flag.fetch_or(true, AcqRel) { - let mut handle = NODES.lock().unwrap(); - if let Some(ref mut set) = *handle { - set.insert(Traced(input, bit)); - } - } +pub fn trace(input: &'static mut (&str, usize)) { + let traced: &mut Traced = unsafe { std::mem::transmute(input) }; + + if traced.1.load(Ordering::Acquire) as u64 == u64::MAX { + // Append to list + LIST.fetch_update(Ordering::AcqRel, Ordering::Acquire, |prev| { + if let Ok(_) = traced.1.fetch_update( + Ordering::AcqRel, + Ordering::Acquire, + |supposed_to_be_max| { + if supposed_to_be_max as u64 == u64::MAX { + Some(prev) + } else { + None + } + }, + ) { + Some(traced) + } else { + None + } + }) + .map(|_| ()) + .unwrap_or_default(); } } -#[no_mangle] pub fn pre_test() { - let mut handle = NODES.lock().unwrap(); - if let Some(set) = handle.replace(HashSet::new()) { - set.into_iter().for_each(|Traced(_, bit)| { - // Reset bit-flag - - // SAFETY: We are given a reference to a u8 which has the same memory representation as bool, - // and therefore also AtomicBool. - let flag: &'static AtomicBool = unsafe { std::mem::transmute(bit) }; - flag.store(false, Release); - }); - } + reset_list(); } #[no_mangle] #[cfg(unix)] -pub fn pre_main() { - // Do not overwrite the HashSet in case it is present - // This may be the case if main() is called directly by a test fn - let mut handle = NODES.lock().unwrap(); - if handle.is_none() { - *handle = Some(HashSet::new()); - } -} +pub fn pre_main() {} #[no_mangle] pub fn post_test(test_name: &str) { - export_traces(|path_buf| get_traces_path(path_buf, test_name), false); + let traces = reset_list(); + export_traces( + traces, + |path_buf| get_traces_path(path_buf, test_name), + false, + ); } #[no_mangle] @@ -86,47 +69,70 @@ pub fn post_test(test_name: &str) { pub fn post_main() { use std::os::unix::process::parent_id; + let traces = read_list(); + let ppid = parent_id(); - export_traces(|path_buf| get_process_traces_path(path_buf, &ppid), true); + export_traces( + traces, + |path_buf| get_process_traces_path(path_buf, &ppid), + true, + ); } -pub fn export_traces(path_buf_init: F, append: bool) +fn read_list<'a>() -> HashSet> { + let mut traces = HashSet::new(); + + let mut ptr = LIST.load(Ordering::Acquire); + while let Some(traced) = unsafe { ptr.as_ref() } { + traces.insert(Cow::Borrowed(traced.0.clone())); + ptr = traced.1.load(Ordering::Acquire); + } + + traces +} + +fn reset_list<'a>() -> HashSet> { + let mut traces = HashSet::new(); + + while let Ok(prev) = LIST.fetch_update(Ordering::AcqRel, Ordering::Acquire, |prev| { + let Traced(_str, next_ptr) = unsafe { prev.as_ref() }?; + Some(next_ptr.load(Ordering::Acquire)) + }) { + let Traced(name, ptr) = unsafe { prev.as_ref() }.unwrap(); + traces.insert(Cow::Borrowed((*name).clone())); + ptr.store(u64::MAX as *mut Traced, Ordering::Release); + } + + traces +} + +fn export_traces<'a, F>(traces: HashSet>, path_buf_init: F, append: bool) where F: FnOnce(PathBuf) -> PathBuf, { - let handle = NODES.lock().unwrap(); - if let Some(ref set) = *handle { - let path_buf = get_dynamic_path(true); - - let mut all = HashSet::new(); - - set.iter().for_each(|Traced(node, _)| { - // Append node to acc - all.insert(node.to_string()); - }); - - #[cfg(unix)] - { - use std::process::id; - - let pid = id(); - let path_child_traces = get_process_traces_path(path_buf.clone(), &pid); - if path_child_traces.is_file() { - read_to_string(path_child_traces) - .unwrap() - .lines() - .for_each(|l| { - all.insert(l.to_string()); - }); - } + let path_buf = get_dynamic_path(true); + let mut traces = traces; + + #[cfg(unix)] + { + use std::process::id; + let pid = id(); + let path_child_traces = get_process_traces_path(path_buf.clone(), &pid); + if path_child_traces.is_file() { + read_to_string(path_child_traces) + .unwrap() + .lines() + .for_each(|l| { + traces.insert(Cow::Owned(l.to_string())); + }); } + } - let output = all.into_iter().fold(String::new(), |mut acc, node| { - acc.push_str(&node); - acc.push_str("\n"); - acc - }); + let output = traces.into_iter().fold(String::new(), |mut acc, node| { + acc.push_str(&node); + acc.push_str("\n"); + acc + }); - write_to_file(output, path_buf, path_buf_init, append); - } + write_to_file(output, path_buf, path_buf_init, append); } diff --git a/rustyrts-dynamic-runner/rust-toolchain.toml b/rustyrts-dynamic-runner/rust-toolchain.toml new file mode 120000 index 00000000..43fe0ada --- /dev/null +++ b/rustyrts-dynamic-runner/rust-toolchain.toml @@ -0,0 +1 @@ +../rust-toolchain.toml \ No newline at end of file diff --git a/rustyrts-dynamic-runner/src/libtest.rs b/rustyrts-dynamic-runner/src/libtest.rs index a4f1cbcc..38c65bcf 100644 --- a/rustyrts-dynamic-runner/src/libtest.rs +++ b/rustyrts-dynamic-runner/src/libtest.rs @@ -71,16 +71,16 @@ pub struct CustomTestDesc { pub name: CustomTestName, pub ignore: bool, pub ignore_message: Option<&'static str>, - // #[cfg(not(bootstrap))] - // pub source_file: &'static str, - // #[cfg(not(bootstrap))] - // pub start_line: usize, - // #[cfg(not(bootstrap))] - // pub start_col: usize, - // #[cfg(not(bootstrap))] - // pub end_line: usize, - // #[cfg(not(bootstrap))] - // pub end_col: usize, + #[cfg(not(bootstrap))] + pub source_file: &'static str, + #[cfg(not(bootstrap))] + pub start_line: usize, + #[cfg(not(bootstrap))] + pub start_col: usize, + #[cfg(not(bootstrap))] + pub end_line: usize, + #[cfg(not(bootstrap))] + pub end_col: usize, pub should_panic: CustomShouldPanic, pub compile_fail: bool, pub no_run: bool, diff --git a/rustyrts-dynamic-runner/src/test_runner.rs b/rustyrts-dynamic-runner/src/test_runner.rs index ecc31680..da411b5a 100644 --- a/rustyrts-dynamic-runner/src/test_runner.rs +++ b/rustyrts-dynamic-runner/src/test_runner.rs @@ -215,9 +215,11 @@ fn execute_tests_unix( drop(rx); install_kill_hook(); - unsafe { - close(std::io::stdout().as_raw_fd()); - close(std::io::stderr().as_raw_fd()); + if !opts.nocapture { + unsafe { + close(std::io::stdout().as_raw_fd()); + close(std::io::stderr().as_raw_fd()); + } } let completed_test = run_test( @@ -299,7 +301,7 @@ fn execute_tests_single_threaded( get_process_traces_path(path_buf.clone(), &pid) }); - remove_file(path_child_traces); + let _ = remove_file(path_child_traces); } let completed_test = run_test( diff --git a/src/bin/cargo-rustyrts.rs b/src/bin/cargo-rustyrts.rs index d02f6374..0b58cb24 100644 --- a/src/bin/cargo-rustyrts.rs +++ b/src/bin/cargo-rustyrts.rs @@ -1,23 +1,19 @@ use itertools::Itertools; -use rustyrts::checksums::Checksums; -use rustyrts::constants::{ - DESC_FLAG, ENDING_CHANGES, ENDING_CHECKSUM, ENDING_GRAPH, ENDING_PROCESS_TRACE, ENDING_TEST, +use rustyrts::{constants::{ + DESC_FLAG, ENDING_CHANGES, ENDING_PROCESS_TRACE, ENDING_TEST, ENDING_TRACE, ENV_RUSTC_WRAPPER, ENV_RUSTYRTS_ARGS, ENV_RUSTYRTS_MODE, ENV_RUSTYRTS_VERBOSE, - ENV_SKIP_ANALYSIS, ENV_TARGET_DIR, FILE_COMPLETE_GRAPH, VERBOSE_COUNT, -}; + ENV_SKIP_ANALYSIS, ENV_TARGET_DIR,VERBOSE_COUNT, ENDING_GRAPH, FILE_COMPLETE_GRAPH, ENDING_CHECKSUM, +}, static_rts::graph::DependencyGraph, fs_utils::read_lines_filter_map, checksums::Checksums}; use rustyrts::fs_utils::{ - get_dynamic_path, get_static_path, get_target_dir, read_lines, read_lines_filter_map, + get_dynamic_path, get_static_path, get_target_dir, read_lines, }; -use rustyrts::static_rts::graph::DependencyGraph; -use rustyrts::utils; use serde_json; -use std::collections::HashSet; +use std::{collections::HashSet, fs::{OpenOptions, read}, io::Write}; use std::ffi::OsString; use std::fs::{ - create_dir_all, read, read_dir, read_to_string, remove_dir_all, remove_file, DirEntry, - OpenOptions, + create_dir_all, read_dir, read_to_string, remove_dir_all, remove_file, DirEntry, + }; -use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -108,10 +104,12 @@ fn rustyrts_dynamic() -> Command { } fn cargo() -> Command { - Command::new(std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"))) + let mut cmd = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"))); + cmd.env("RUSTYRTS_SKIP_BUILD", "true"); + cmd } -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy)] enum Mode { Clean, Dynamic, @@ -312,7 +310,9 @@ fn main() { return; } - if let Some("rustyrts") = std::env::args().nth(1).as_ref().map(AsRef::as_ref) { + if let Some(arg1) = std::env::args().nth(1) { + + if arg1 == "rustyrts" { let mode_string = std::env::args().nth(2).unwrap_or("".to_string()); let mode = FromStr::from_str(&mode_string).ok(); @@ -320,19 +320,17 @@ fn main() { Some(Mode::Clean) => { clean(); } - Some(Mode::Static) => { - // This arm is for when `cargo rustyrts static` is called. We call `cargo build`, + Some(mode) => { + // This arm is for when `cargo rustyrts (static|dynamic)` is called. We call `cargo build`, // but with the `RUSTC` env var set to the `cargo-rustyrts` binary so that we come back in the other branch, - // and dispatch the invocations to `rustyrts-static`, respectively. - run_cargo_rustc_static(); - select_and_execute_tests_static(); - } - Some(Mode::Dynamic) => { - // This arm is for when `cargo rustyrts dynamic` is called. We call `cargo build`, - // but with the `RUSTC` env var set to the `cargo-rustyrts` binary so that we come back in the other branch, - // and dispatch the invocations to `rustyrts-dynamic`, respectively. - run_cargo_rustc_dynamic(); - select_and_execute_tests_dynamic(); + // and dispatch the invocations to `rustyrts-(static|dnyamic)`, respectively. + run_cargo_build(mode); + match mode { + Mode::Static=> select_and_execute_tests_static(), + Mode::Dynamic => select_and_execute_tests_dynamic(), + Mode::Clean=> unreachable!(), + } + } _ => { show_error( @@ -341,7 +339,7 @@ fn main() { ) } } - } else if let Some("rustc") = std::env::args().nth(1).as_ref().map(AsRef::as_ref) { + } else if &arg1[arg1.len()-5..] == "rustc" { // This arm is executed when `cargo-rustyrts` runs `cargo build` or `cargo test` with the `RUSTC_WRAPPER` env var set to itself. run_rustyrts(); } else { @@ -350,6 +348,8 @@ fn main() { .to_string(), ) } + } + } //###################################################################################################################### @@ -390,11 +390,6 @@ fn run_rustyrts() { cmd.args(std::env::args().skip(2)); // skip `cargo rustc` - // Add sysroot - let sysroot = utils::compile_time_sysroot().expect("Cannot find sysroot"); - cmd.arg("--sysroot"); - cmd.arg(sysroot); - // Add args for `rustyrts` let rustyrts_args_raw = std::env::var(ENV_RUSTYRTS_ARGS).expect(&format!("missing {}", ENV_RUSTYRTS_ARGS)); @@ -417,14 +412,15 @@ fn run_rustyrts() { } } -//###################################################################################################################### -// STATIC RTS - /// This will construct and execute a command like: -/// `cargo build --bin some_crate_name -v -- --top_crate_name some_top_crate_name --domain interval -v cargo-rustyrts-marker-end` -/// using the rustc wrapper for static rustyrts -fn run_cargo_rustc_static() { - let path_buf: PathBuf = get_static_path(false); +/// `cargo build --bin some_crate_name -v -- cargo-rustyrts-marker-begin --top_crate_name some_top_crate_name --domain interval -v` +/// using the rustc wrapper for dynamic rustyrts +fn run_cargo_build(mode: Mode){ + let path_buf = match mode { + Mode::Clean => unreachable!(), + Mode::Dynamic => get_dynamic_path(false), + Mode::Static => get_static_path(false), + }; create_dir_all(path_buf.as_path()).expect(&format!( "Failed to create directory {}", @@ -434,16 +430,30 @@ fn run_cargo_rustc_static() { let files = read_dir(path_buf.as_path()).unwrap(); for path_res in files { if let Ok(path) = path_res { - if path.file_name().to_str().unwrap().ends_with(ENDING_CHANGES) { + let file_name = path.file_name(); + if file_name.to_str().unwrap().ends_with(ENDING_CHANGES) { remove_file(path.path()).unwrap(); } + + #[cfg(unix)] + if mode == Mode::Dynamic { + if file_name.to_str().unwrap().ends_with(ENDING_PROCESS_TRACE) { + remove_file(path.path()).unwrap(); + } + } } } - let cmd = cargo_build(Mode::Static); + // Now we run `cargo build $FLAGS $ARGS`, giving the user the + // chance to add additional arguments. `FLAGS` is set to identify + // this target. The user gets to control what gets actually passed to rustyrts. + let cmd = cargo_build(mode); execute(cmd); } +//###################################################################################################################### +// STATIC RTS + /// This will construct and execute a command like: /// * `cargo test --no-fail-fast -- --exact test_1 test_2 ...` (If some tests are affected) /// * `cargo test --no-fail-fast --no-run` (If no tests are affected) @@ -535,8 +545,6 @@ fn select_and_execute_tests_static() { println!("#Tests that have been found: {}\n", tests.iter().count()); - #[cfg(not(feature = "print_paths"))] - { let reached_nodes = dependency_graph.reachable_nodes(changed_nodes); let affected_tests: HashSet<&&String> = tests.intersection(&reached_nodes).collect(); @@ -557,99 +565,20 @@ fn select_and_execute_tests_static() { let cmd = cargo_test(Mode::Static, affected_tests.into_iter().map(|test| *test)); execute(cmd); - } - #[cfg(feature = "print_paths")] - { - let (reached_nodes, affected_tests) = dependency_graph.affected_tests(changed_nodes, tests); - - println!( - "#Nodes that reach any changed node in the graph: {}\n", - reached_nodes.iter().count() - ); - - if verbose { - println!( - "Affected tests:\n{}\n", - affected_tests - .iter() - .sorted() - .map(|(k, v)| { - let path = format!( - "{}: < {}{} >", - k, - v[0], - if v.len() >= VERBOSE_COUNT { - format!( - "<- ... <- {}", - v[v.len() - VERBOSE_COUNT..].iter().join(" <- ") - ) - } else { - v[1..].iter().join(" <- ") - } - ); - - format!("{}: {}", k, path) - }) - .join("\n") - ); - } else { - println!("#Affected tests: {}\n", affected_tests.iter().count()); - } - - let cmd = cargo_test( - Mode::Static, - affected_tests.keys().into_iter().map(|test| *test), - ); - - execute(cmd); - } } //###################################################################################################################### // DYNAMIC RTS -/// This will construct and execute a command like: -/// `cargo build --bin some_crate_name -v -- cargo-rustyrts-marker-begin --top_crate_name some_top_crate_name --domain interval -v cargo-rustyrts-marker-end` -/// using the rustc wrapper for dynamic rustyrts -fn run_cargo_rustc_dynamic() { - let path_buf: PathBuf = get_dynamic_path(false); - - create_dir_all(path_buf.as_path()).expect(&format!( - "Failed to create directory {}", - path_buf.display() - )); - - let files = read_dir(path_buf.as_path()).unwrap(); - for path_res in files { - if let Ok(path) = path_res { - let file_name = path.file_name(); - if file_name.to_str().unwrap().ends_with(ENDING_CHANGES) { - remove_file(path.path()).unwrap(); - } - - #[cfg(unix)] - if file_name.to_str().unwrap().ends_with(ENDING_PROCESS_TRACE) { - remove_file(path.path()).unwrap(); - } - } - } - - // Now we run `cargo build $FLAGS $ARGS`, giving the user the - // chance to add additional arguments. `FLAGS` is set to identify - // this target. The user gets to control what gets actually passed to rustyrts. - let cmd = cargo_build(Mode::Dynamic); - execute(cmd); -} - // This will construct command line like: // either `cargo test --no-fail-fast -- --exact test_1 test_2 ...` (If some tests are affected) // or `cargo test --no-fail-fast --no-run` (If no tests are affected) -/// using the rustc wrapper for dynamic rustyrts +/// using the respective rustc wrapper fn select_and_execute_tests_dynamic() { let verbose = has_arg_flag("-v"); let path_buf = get_dynamic_path(true); - + let files: Vec = read_dir(path_buf.as_path()) .unwrap() .map(|maybe_path| maybe_path.unwrap()) @@ -675,9 +604,12 @@ fn select_and_execute_tests_dynamic() { println!("#Tests that have been found: {}\n", tests.iter().count()); - // Read traces + // Read traces or dependencies + let ending = ENDING_TRACE; + let mut affected_tests: Vec<(String, Option>)> = Vec::new(); - let traced_tests: Vec<&DirEntry> = files + + let analyzed_tests: Vec<&DirEntry> = files .iter() .filter(|traces| { traces @@ -685,11 +617,11 @@ fn select_and_execute_tests_dynamic() { .to_os_string() .into_string() .unwrap() - .ends_with(ENDING_TRACE) + .ends_with(ending) }) .collect(); - let traced_tests_names: HashSet = traced_tests + let analyzed_tests_names: HashSet = analyzed_tests .iter() .map(|f| { f.file_name() @@ -707,18 +639,18 @@ fn select_and_execute_tests_dynamic() { .collect(); println!( - "#Tests with traces:: {}\n", - traced_tests_names.iter().count() + "#Tests with information: {}\n", + analyzed_tests_names.iter().count() ); affected_tests.append( &mut tests - .difference(&traced_tests_names) + .difference(&analyzed_tests_names) .map(|s| (s.clone(), None)) .collect_vec(), ); - for file in traced_tests { + for file in analyzed_tests { let traced_nodes: HashSet = read_to_string(file.path()) .unwrap() .split("\n") diff --git a/src/bin/rustyrts-dynamic.rs b/src/bin/rustyrts-dynamic.rs index 78c0ac15..39e6f61d 100644 --- a/src/bin/rustyrts-dynamic.rs +++ b/src/bin/rustyrts-dynamic.rs @@ -1,26 +1,15 @@ #![feature(rustc_private)] -extern crate rustc_ast_pretty; extern crate rustc_driver; -extern crate rustc_error_codes; -extern crate rustc_errors; -extern crate rustc_hash; -extern crate rustc_hir; -extern crate rustc_interface; extern crate rustc_log; -extern crate rustc_middle; -extern crate rustc_session; -extern crate rustc_span; -use rustc_session::config::ErrorOutputType; -use rustc_session::early_error; +use rustc_log::LoggerConfig; use rustyrts::callbacks_shared::export_checksums_and_changes; -use rustyrts::constants::ENV_SKIP_ANALYSIS; +use rustyrts::constants::{ENV_BLACKBOX_TEST, ENV_SKIP_ANALYSIS, ENV_TARGET_DIR}; use rustyrts::dynamic_rts::callback::DynamicRTSCallbacks; use rustyrts::format::create_logger; -use rustyrts::utils; -use std::env; use std::process; +use std::{env, path::PathBuf}; //###################################################################################################################### // This file is heavily inspired by rust-mir-checker @@ -34,10 +23,11 @@ pub const EXIT_SUCCESS: i32 = 0; pub const EXIT_FAILURE: i32 = 1; fn main() { - rustc_log::init_rustc_env_logger().unwrap(); + rustc_log::init_logger(LoggerConfig::from_env("RUSTC")).unwrap(); create_logger().init(); - let skip = env::var(ENV_SKIP_ANALYSIS).is_ok(); + let skip = env::var(ENV_SKIP_ANALYSIS).is_ok() + && !(env::var(ENV_TARGET_DIR).map(|var| var.ends_with("trybuild")) == Ok(true)); if !skip { let result = rustc_driver::catch_fatal_errors(move || { @@ -45,32 +35,32 @@ fn main() { .enumerate() .map(|(i, arg)| { arg.into_string().unwrap_or_else(|arg| { - early_error( - ErrorOutputType::default(), - &format!("Argument {} is not valid Unicode: {:?}", i, arg), - ) + eprintln!("Argument {} is not valid Unicode: {:?}", i, arg); + process::exit(EXIT_FAILURE); }) }) + .map(|arg| { + // when running blackbox tests, this ensures that stable crate ids do not change if features are enabled + if std::env::var(ENV_BLACKBOX_TEST).is_ok() { + if arg.starts_with("metadata=") { + return "metadata=".to_string(); + } + } + arg + }) .collect::>(); // Provide information on where to find rustyrts-dynamic-rlib - let cargo_home = std::env::var("CARGO_HOME").unwrap_or("~/.cargo".to_string()); + let mut rlib_source = + PathBuf::from(std::env::var("CARGO_HOME").expect("Did not find CARGO_HOME")); + rlib_source.push("bin"); rustc_args.push("-L".to_string()); - rustc_args.push(format!("{}/bin", cargo_home).to_string()); + rustc_args.push(rlib_source.display().to_string()); rustc_args.push("--cap-lints".to_string()); rustc_args.push("allow".to_string()); - if let Some(sysroot) = utils::compile_time_sysroot() { - let sysroot_flag = "--sysroot"; - if !rustc_args.iter().any(|e| e == sysroot_flag) { - // We need to overwrite the default that librustc would compute. - rustc_args.push(sysroot_flag.to_owned()); - rustc_args.push(sysroot); - } - } - let mut callbacks = DynamicRTSCallbacks::new(); let run_compiler = rustc_driver::RunCompiler::new(&rustc_args, &mut callbacks); diff --git a/src/bin/rustyrts-static.rs b/src/bin/rustyrts-static.rs index 1b6f847e..f202ad91 100644 --- a/src/bin/rustyrts-static.rs +++ b/src/bin/rustyrts-static.rs @@ -2,15 +2,12 @@ extern crate rustc_driver; extern crate rustc_log; -extern crate rustc_session; -use rustc_session::config::ErrorOutputType; -use rustc_session::early_error; -use rustyrts::callbacks_shared::export_checksums_and_changes; -use rustyrts::constants::ENV_SKIP_ANALYSIS; +use rustc_log::LoggerConfig; +use rustyrts::constants::{ENV_SKIP_ANALYSIS, ENV_TARGET_DIR}; use rustyrts::format::create_logger; use rustyrts::static_rts::callback::StaticRTSCallbacks; -use rustyrts::utils; +use rustyrts::{callbacks_shared::export_checksums_and_changes, constants::ENV_BLACKBOX_TEST}; use std::env; use std::process; @@ -26,10 +23,11 @@ pub const EXIT_SUCCESS: i32 = 0; pub const EXIT_FAILURE: i32 = 1; fn main() { - rustc_log::init_rustc_env_logger().unwrap(); + rustc_log::init_logger(LoggerConfig::from_env("RUSTC")).unwrap(); create_logger().init(); - let skip = env::var(ENV_SKIP_ANALYSIS).is_ok(); + let skip = env::var(ENV_SKIP_ANALYSIS).is_ok() + && !(env::var(ENV_TARGET_DIR).map(|var| var.ends_with("trybuild")) == Ok(true)); if !skip { let result = rustc_driver::catch_fatal_errors(move || { @@ -37,23 +35,21 @@ fn main() { .enumerate() .map(|(i, arg)| { arg.into_string().unwrap_or_else(|arg| { - early_error( - ErrorOutputType::default(), - &format!("Argument {} is not valid Unicode: {:?}", i, arg), - ) + eprintln!("Argument {} is not valid Unicode: {:?}", i, arg); + process::exit(EXIT_FAILURE); }) }) + .map(|arg| { + // when running blackbox tests, this ensures that stable crate ids do not change if features are enabled + if std::env::var(ENV_BLACKBOX_TEST).is_ok() { + if arg.starts_with("metadata=") { + return "metadata=".to_string(); + } + } + arg + }) .collect::>(); - if let Some(sysroot) = utils::compile_time_sysroot() { - let sysroot_flag = "--sysroot"; - if !rustc_args.iter().any(|e| e == sysroot_flag) { - // We need to overwrite the default that librustc would compute. - rustc_args.push(sysroot_flag.to_owned()); - rustc_args.push(sysroot); - } - } - rustc_args.push("--cap-lints".to_string()); rustc_args.push("allow".to_string()); diff --git a/src/callbacks_shared.rs b/src/callbacks_shared.rs index 05351652..f5d1dbdf 100644 --- a/src/callbacks_shared.rs +++ b/src/callbacks_shared.rs @@ -2,19 +2,16 @@ use itertools::Itertools; use log::{debug, trace}; use once_cell::sync::OnceCell; use rustc_hir::def_id::LOCAL_CRATE; -use rustc_middle::mir::Body; -use rustc_middle::ty::{GenericArg, List, TyCtxt}; -use rustc_span::def_id::DefId; -use std::collections::HashMap; +use rustc_middle::mir::mono::MonoItem; +use rustc_middle::ty::TyCtxt; use std::env; +use std::path::PathBuf; use std::{ collections::HashSet, fs::read, sync::{atomic::AtomicUsize, Mutex}, }; -use crate::checksums::{get_checksum_body, insert_hashmap}; -use crate::const_visitor::ConstVisitor; use crate::constants::ENV_SKIP_ANALYSIS; use crate::{ checksums::Checksums, @@ -24,11 +21,16 @@ use crate::{ get_test_path, write_to_file, }, names::def_id_name, - static_rts::callback::PATH_BUF, +}; +use crate::{ + checksums::{get_checksum_body, insert_hashmap}, + const_visitor::ResolvingConstVisitor, }; pub(crate) static OLD_VTABLE_ENTRIES: AtomicUsize = AtomicUsize::new(0); +pub(crate) static PATH_BUF: OnceCell = OnceCell::new(); + pub(crate) static CRATE_NAME: OnceCell = OnceCell::new(); pub(crate) static CRATE_ID: OnceCell = OnceCell::new(); @@ -47,7 +49,7 @@ pub(crate) fn excluded String>(getter_crate_name: F) -> bool { *EXCLUDED.get_or_init(|| { let exclude = env::var(ENV_SKIP_ANALYSIS).is_ok() || no_instrumentation(getter_crate_name); if exclude { - trace!("Excluding crate {}", getter_crate_name()); + debug!("Excluding crate {}", getter_crate_name()); } exclude }) @@ -65,41 +67,58 @@ pub(crate) fn no_instrumentation String>(getter_crate_name: F) let no_instrumentation = excluded_crate || trybuild; if no_instrumentation { - trace!("Not instrumenting crate {}", getter_crate_name()); + debug!("Not instrumenting crate {}", getter_crate_name()); } no_instrumentation }) } -pub(crate) fn run_analysis_shared<'tcx>( - tcx: TyCtxt<'tcx>, - bodies: Vec<(&'tcx Body<'tcx>, &'tcx List>)>, -) { +pub(crate) fn run_analysis_shared<'tcx>(tcx: TyCtxt<'tcx>) { let crate_name = format!("{}", tcx.crate_name(LOCAL_CRATE)); - let crate_id = tcx.sess.local_stable_crate_id().to_u64(); + let crate_id = tcx.stable_crate_id(LOCAL_CRATE).as_u64(); - CRATE_NAME.get_or_init(|| crate_name.clone()); - CRATE_ID.get_or_init(|| crate_id); + //############################################################################################################## + // Collect all MIR bodies that are relevant for code generation - let mut bodies_map: HashMap = HashMap::new(); + let code_gen_units = tcx.collect_and_partition_mono_items(()).1; - for (body, _substs) in &bodies { - bodies_map.insert(body.source.def_id(), body); - } - let dedup_bodies: Vec<&'tcx Body> = bodies_map.values().map(|b| *b).collect(); + let bodies = code_gen_units + .iter() + .flat_map(|c| c.items().keys()) + .filter(|m| if let MonoItem::Fn(_) = m { true } else { false }) + .map(|m| { + let MonoItem::Fn(instance) = m else { + unreachable!() + }; + instance + }) + .map(|i| i.def_id()) + //.filter(|d| d.is_local()) // It is not feasible to only analyze local MIR + .filter(|d| tcx.is_mir_available(d)) + .unique() + .map(|d| tcx.optimized_mir(d)) + .collect_vec(); + + //############################################################################################################## + // Continue at shared analysis + + CRATE_NAME.get_or_init(|| crate_name.clone()); + CRATE_ID.get_or_init(|| crate_id); //########################################################################################################## // 2. Calculate checksum of every MIR body and the consts that it uses - let mut const_visitor = ConstVisitor::new(tcx); - for (body, substs) in &bodies { - const_visitor.visit(&body, substs); - } - let mut new_checksums = NEW_CHECKSUMS.get().unwrap().lock().unwrap(); + let mut new_checksums_const = NEW_CHECKSUMS_CONST.get().unwrap().lock().unwrap(); + + for body in &bodies { + let name = def_id_name(tcx, body.source.def_id(), false, true); + + let checksums_const = ResolvingConstVisitor::find_consts(tcx, body); + for checksum in checksums_const { + insert_hashmap(&mut *new_checksums_const, &name, checksum); + } - for body in dedup_bodies { - let name = def_id_name(tcx, body.source.def_id(), List::empty(), false, true); // IMPORTANT: no substs here let checksum = get_checksum_body(tcx, body); insert_hashmap(&mut *new_checksums, &name, checksum); } @@ -111,7 +130,7 @@ pub(crate) fn run_analysis_shared<'tcx>( for def_id in tcx.mir_keys(()) { for attr in tcx.get_attrs_unchecked(def_id.to_def_id()) { if attr.name_or_empty().to_ident_string() == TEST_MARKER { - tests.push(def_id_name(tcx, def_id.to_def_id(), &[], false, false)); + tests.push(def_id_name(tcx, def_id.to_def_id(), false, false)); } } } @@ -125,7 +144,7 @@ pub(crate) fn run_analysis_shared<'tcx>( ); } - trace!("Exported tests for {}", crate_name); + debug!("Exported tests for {}", crate_name); } pub fn export_checksums_and_changes(from_new_revision: bool) { @@ -178,15 +197,13 @@ pub fn export_checksums_and_changes(from_new_revision: bool) { } }; - trace!("Imported checksums for {}", crate_name); + debug!("Imported checksums for {}", crate_name); //############################################################################################################## // 4. Calculate names of changed nodes and write this information to filesystem let mut changed_nodes = HashSet::new(); - trace!("Checksums: {:?}", new_checksums); - // We only consider nodes from the new revision // (Dynamic: if something in the old revision has been removed, there must be a change to some other function) for name in new_checksums.keys() { @@ -293,6 +310,6 @@ pub fn export_checksums_and_changes(from_new_revision: bool) { false, ); - trace!("Exported changes for {}", crate_name); + debug!("Exported changes for {}", crate_name); } } diff --git a/src/const_visitor.rs b/src/const_visitor.rs index 14cf3970..596d6212 100644 --- a/src/const_visitor.rs +++ b/src/const_visitor.rs @@ -1,60 +1,76 @@ +use std::collections::HashSet; + use rustc_hir::definitions::DefPathData; -use rustc_middle::mir::interpret::ConstAllocation; -use rustc_middle::mir::interpret::{ConstValue, GlobalAlloc, Scalar}; -use rustc_middle::ty::{GenericArg, List, ScalarInt}; use rustc_middle::{ - mir::{visit::Visitor, Body, Constant, ConstantKind, Location}, + mir::{interpret::ConstAllocation, ConstValue}, + ty::{EarlyBinder, ParamEnv}, +}; +use rustc_middle::{ + mir::{ + interpret::{GlobalAlloc, Scalar}, + ConstOperand, + }, + ty::{Instance, TyKind}, +}; +use rustc_middle::{ + mir::{visit::TyContext, Const}, + ty::{GenericArg, List, ScalarInt, Ty}, +}; +use rustc_middle::{ + mir::{visit::Visitor, Body, Location}, ty::TyCtxt, }; use rustc_span::def_id::DefId; use crate::checksums::{get_checksum_const_allocation, get_checksum_scalar_int}; -use crate::{callbacks_shared::NEW_CHECKSUMS_CONST, checksums::insert_hashmap, names::def_id_name}; -pub(crate) struct ConstVisitor<'tcx> { +pub struct ResolvingConstVisitor<'tcx> { tcx: TyCtxt<'tcx>, - processed_instance: Option<(DefId, &'tcx List>)>, - - #[cfg(not(feature = "monomorphize"))] - original_substs: Option<&'tcx List>>, + param_env: ParamEnv<'tcx>, + acc: HashSet<(u64, u64)>, + visited: HashSet<(DefId, &'tcx List>)>, + substs: &'tcx List>, + processed: Option, } -impl<'tcx> ConstVisitor<'tcx> { - pub fn new(tcx: TyCtxt<'tcx>) -> ConstVisitor<'tcx> { - Self { +impl<'tcx, 'g> ResolvingConstVisitor<'tcx> { + pub(crate) fn find_consts(tcx: TyCtxt<'tcx>, body: &'tcx Body<'tcx>) -> HashSet<(u64, u64)> { + let def_id = body.source.def_id(); + let param_env = tcx.param_env(def_id).with_reveal_all_normalized(tcx); + let mut resolver = ResolvingConstVisitor { tcx, - processed_instance: None, - - #[cfg(not(feature = "monomorphize"))] - original_substs: None, + param_env, + acc: HashSet::new(), + visited: HashSet::new(), + substs: List::identity_for_item(tcx, def_id), + processed: None, + }; + + resolver.visit_body(body); + for body in tcx.promoted_mir(def_id) { + resolver.visit_body(body) } + resolver.acc } - pub fn visit(&mut self, body: &Body<'tcx>, substs: &'tcx List>) { - let def_id = body.source.instance.def_id(); + fn visit(&mut self, def_id: DefId, substs: &'tcx List>) { + if self.visited.insert((def_id, substs)) { + if self.tcx.is_mir_available(def_id) { + let old_processed = self.processed; + self.processed = Some(def_id); - #[cfg(feature = "monomorphize")] - { - self.processed_instance = Some((def_id, substs)); - } - #[cfg(not(feature = "monomorphize"))] - { - self.processed_instance = Some((def_id, List::empty())); - self.original_substs = Some(substs); - } - //############################################################################################################## - // Visit body and contained promoted mir + let old_substs = self.substs; + self.substs = substs; - self.super_body(body); - for body in self.tcx.promoted_mir(def_id) { - self.super_body(body) - } - - self.processed_instance = None; + let body = self.tcx.optimized_mir(def_id); + self.visit_body(body); + for body in self.tcx.promoted_mir(def_id) { + self.visit_body(body) + } - #[cfg(not(feature = "monomorphize"))] - { - self.original_substs = None; + self.substs = old_substs; + self.processed = old_processed; + } } } @@ -66,7 +82,7 @@ impl<'tcx> ConstVisitor<'tcx> { match value { ConstValue::Scalar(scalar) => match scalar { Scalar::Ptr(ptr, _) => { - let global_alloc = self.tcx.global_alloc(ptr.provenance); + let global_alloc = self.tcx.global_alloc(ptr.provenance.alloc_id()); match global_alloc { GlobalAlloc::Static(def_id) => { // If the def path contains a foreign mod, it cannot be computed at compile time @@ -89,59 +105,63 @@ impl<'tcx> ConstVisitor<'tcx> { }, ConstValue::Slice { data: allocation, - start: _, - end: _, + meta: _, } => Some(Ok(allocation)), - ConstValue::ByRef { - alloc: allocation, + ConstValue::Indirect { + alloc_id: allocation, offset: _, - } => Some(Ok(allocation)), + } => { + // TODO: check this + let global_alloc = self.tcx.global_alloc(allocation); + match global_alloc { + GlobalAlloc::Static(def_id) => { + // If the def path contains a foreign mod, it cannot be computed at compile time + let def_path = self.tcx.def_path(def_id); + if def_path + .data + .iter() + .any(|d| d.data == DefPathData::ForeignMod) + { + return None; + } + + self.tcx.eval_static_initializer(def_id).ok().map(|s| Ok(s)) + } + GlobalAlloc::Memory(const_alloc) => Some(Ok(const_alloc)), + _ => None, + } + } _ => None, } } } -impl<'tcx> Visitor<'tcx> for ConstVisitor<'tcx> { - fn visit_constant(&mut self, constant: &Constant<'tcx>, location: Location) { +impl<'tcx> Visitor<'tcx> for ResolvingConstVisitor<'tcx> { + fn visit_constant(&mut self, constant: &ConstOperand<'tcx>, location: Location) { self.super_constant(constant, location); - let (def_id, substs) = self.processed_instance.unwrap(); - let literal = constant.literal; - - if let Some(allocation_or_int) = match literal { - ConstantKind::Val(cons, _ty) => self.maybe_const_alloc_from_const_value(cons), - ConstantKind::Unevaluated(mut unevaluated_cons, _) => { - let param_env = self - .tcx - .param_env(def_id) - .with_reveal_all_normalized(self.tcx); - - #[cfg(not(feature = "monomorphize"))] - { - unevaluated_cons = self.tcx.subst_and_normalize_erasing_regions( - self.original_substs.unwrap(), - param_env, - unevaluated_cons, - ); - } - - #[cfg(feature = "monomorphize")] - { - unevaluated_cons = self.tcx.subst_and_normalize_erasing_regions( - substs, - param_env, - unevaluated_cons, - ); - } - - self.tcx - .const_eval_resolve(param_env, unevaluated_cons, None) - .map(|c| self.maybe_const_alloc_from_const_value(c)) - .unwrap_or(None) + let literal = constant.const_; + + let maybe_allocation_or_int = match literal { + Const::Val(cons, _ty) => self.maybe_const_alloc_from_const_value(cons), + Const::Unevaluated(unevaluated_cons, _) => { + let maybe_normalized_cons = self.tcx.try_instantiate_and_normalize_erasing_regions( + self.substs, + self.param_env, + EarlyBinder::bind(unevaluated_cons), + ); + + maybe_normalized_cons.ok().and_then(|unevaluated_cons| { + self.tcx + .const_eval_resolve(self.param_env, unevaluated_cons, None) + .ok() + .and_then(|c| self.maybe_const_alloc_from_const_value(c)) + }) } _ => None, - } { - let name: String = def_id_name(self.tcx, def_id, substs, false, true); + }; + + if let Some(allocation_or_int) = maybe_allocation_or_int { let checksum = match allocation_or_int { Ok(allocation) => get_checksum_const_allocation(self.tcx, &allocation), Err(scalar_int) => { @@ -149,12 +169,83 @@ impl<'tcx> Visitor<'tcx> for ConstVisitor<'tcx> { checksum } }; + self.acc.insert(checksum); + } + } + + fn visit_ty(&mut self, ty: Ty<'tcx>, _ty_context: TyContext) { + self.super_ty(ty); + + if let Some(outer_def_id) = self.processed { + match *ty.kind() { + TyKind::Closure(..) | TyKind::Coroutine(..) | TyKind::FnDef(..) => { + // We stop recursing when the function can also be resolved + // using the environment of the currently visited function + let param_env_outer = self + .tcx + .param_env(outer_def_id) + .with_reveal_all_normalized(self.tcx); + + let maybe_normalized_ty = match *ty.kind() { + TyKind::Closure(..) | TyKind::Coroutine(..) | TyKind::FnDef(..) => self + .tcx + .try_instantiate_and_normalize_erasing_regions( + List::identity_for_item(self.tcx, outer_def_id), + param_env_outer, + EarlyBinder::bind(ty), + ) + .ok(), + _ => None, + }; + + if let Some(ty_outer) = maybe_normalized_ty { + let (TyKind::Closure(def_id, substs) + | TyKind::Coroutine(def_id, substs, _) + | TyKind::FnDef(def_id, substs)) = *ty_outer.kind() + else { + unreachable!() + }; + if let Ok(Some(_)) | Err(_) = + Instance::resolve(self.tcx, param_env_outer, def_id, substs) + { + return; + } + } + } + _ => {} + } + } + + let maybe_next = { + let maybe_normalized_ty = match *ty.kind() { + TyKind::Closure(..) | TyKind::Coroutine(..) | TyKind::FnDef(..) => self + .tcx + .try_instantiate_and_normalize_erasing_regions( + self.substs, + self.param_env, + EarlyBinder::bind(ty), + ) + .ok(), + _ => None, + }; + + maybe_normalized_ty.and_then(|ty| match *ty.kind() { + TyKind::Closure(def_id, substs) + | TyKind::Coroutine(def_id, substs, _) + | TyKind::FnDef(def_id, substs) => { + match Instance::resolve(self.tcx, self.param_env, def_id, substs) { + Ok(Some(instance)) if !self.tcx.is_closure(instance.def_id()) => { + Some((instance.def.def_id(), instance.args)) + } + _ => None, + } + } + _ => None, + }) + }; - insert_hashmap( - &mut *NEW_CHECKSUMS_CONST.get().unwrap().lock().unwrap(), - &name, - checksum, - ); + if let Some((def_id, substs)) = maybe_next { + self.visit(def_id, substs); } } } diff --git a/src/constants.rs b/src/constants.rs index c114b0bb..153d4ddb 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -17,7 +17,7 @@ pub const ENV_RUSTC_WRAPPER: &str = "RUSTC_WRAPPER"; pub const ENV_TARGET_DIR: &str = "CARGO_TARGET_DIR"; /// Used to buffer arguments to rustc -pub const ENV_RUSTYRTS_ARGS: &str = "rustyrts_args"; +pub const ENV_RUSTYRTS_ARGS: &str = "RUSTYRTS_ARGS"; /// Used to specify whether rustyrts should provide verbose output pub const ENV_RUSTYRTS_VERBOSE: &str = "RUSTYRTS_VERBOSE"; @@ -28,6 +28,8 @@ pub const ENV_RUSTYRTS_LOG: &str = "RUSTYRTS_LOG"; /// Used to skip the analysis in the second invocation of the compiler wrapper pub const ENV_SKIP_ANALYSIS: &str = "RUSTYRTS_SKIP"; +pub const ENV_BLACKBOX_TEST: &str = "RUSTYRTS_BLACKBOX_TEST"; + //###################################################################################################################### // File endings or names @@ -36,6 +38,7 @@ pub const DIR_DYNAMIC: &str = ".rts_dynamic"; pub const FILE_COMPLETE_GRAPH: &str = "!complete_graph.dot"; +pub const ENDING_DEPENDENCIES: &str = ".dependencies"; pub const ENDING_TRACE: &str = ".trace"; pub const ENDING_CHANGES: &str = ".changes"; pub const ENDING_CHECKSUM: &str = ".checksum"; diff --git a/src/dynamic_rts/callback.rs b/src/dynamic_rts/callback.rs index fb8d53cb..5d109d3d 100644 --- a/src/dynamic_rts/callback.rs +++ b/src/dynamic_rts/callback.rs @@ -1,32 +1,40 @@ -use itertools::Itertools; -use log::trace; +use log::{debug, trace}; +use rustc_ast::{ + token::{Delimiter, Token, TokenKind}, + tokenstream::{DelimSpan, Spacing, TokenStream, TokenTree}, + AttrArgs, AttrStyle, Crate, DelimArgs, Path, PathSegment, +}; +use rustc_attr::mk_attr; use rustc_data_structures::sync::Ordering::SeqCst; use rustc_driver::{Callbacks, Compilation}; +use rustc_feature::Features; use rustc_interface::{interface, Queries}; -use rustc_middle::mir::mono::MonoItem; -use rustc_middle::ty::query::{query_keys, query_stored}; +use rustc_middle::mir::Body; use rustc_middle::ty::{PolyTraitRef, TyCtxt, VtblEntry}; use rustc_session::config::CrateType; -use rustc_span::source_map::{FileLoader, RealFileLoader}; +use rustc_span::{ + source_map::{FileLoader, RealFileLoader}, + sym::{self}, + symbol::Ident, + Symbol, DUMMY_SP, +}; use std::mem::transmute; -use std::sync::atomic::AtomicUsize; use std::sync::Mutex; +use std::sync::atomic::AtomicUsize; use crate::callbacks_shared::{ - excluded, no_instrumentation, run_analysis_shared, EXCLUDED, NEW_CHECKSUMS, - NEW_CHECKSUMS_CONST, NEW_CHECKSUMS_VTBL, OLD_VTABLE_ENTRIES, -}; + excluded, no_instrumentation, run_analysis_shared, EXCLUDED, NEW_CHECKSUMS, + NEW_CHECKSUMS_CONST, NEW_CHECKSUMS_VTBL, OLD_VTABLE_ENTRIES, PATH_BUF, + }; +use super::file_loader::{InstrumentationFileLoaderProxy, TestRunnerFileLoaderProxy}; use crate::checksums::{get_checksum_vtbl_entry, insert_hashmap, Checksums}; use crate::dynamic_rts::instrumentation::modify_body; use crate::fs_utils::get_dynamic_path; use crate::names::def_id_name; -use crate::static_rts::callback::PATH_BUF; -use rustc_hir::def_id::LOCAL_CRATE; +use rustc_hir::def_id::{LocalDefId, LOCAL_CRATE}; -use super::file_loader::{InstrumentationFileLoaderProxy, TestRunnerFileLoaderProxy}; - -static OLD_OPTIMIZED_MIR_PTR: AtomicUsize = AtomicUsize::new(0); +static OLD_OPTIMIZED_MIR: AtomicUsize = AtomicUsize::new(0); pub struct DynamicRTSCallbacks {} @@ -46,7 +54,7 @@ impl Callbacks for DynamicRTSCallbacks { .iter() .any(|t| *t == CrateType::ProcMacro) { - trace!( + debug!( "Excluding crate {}", config.opts.crate_name.as_ref().unwrap() ); @@ -74,9 +82,9 @@ impl Callbacks for DynamicRTSCallbacks { // We need to replace this in any case, since we also want to instrument rlib crates // Further, the only possibility to intercept vtable entries, which I found, is in their local crate - config.override_queries = Some(|_session, providers, _extern_providers| { - // SAFETY: We store the address of the original optimized_mir function as a usize. - OLD_OPTIMIZED_MIR_PTR.store(unsafe { transmute(providers.optimized_mir) }, SeqCst); + config.override_queries = Some(|_session, providers| { + // SAFETY: We store the addressses of the original functions as a usize. + OLD_OPTIMIZED_MIR.store(unsafe { transmute(providers.optimized_mir) }, SeqCst); OLD_VTABLE_ENTRIES.store(unsafe { transmute(providers.vtable_entries) }, SeqCst); providers.optimized_mir = custom_optimized_mir; @@ -84,6 +92,74 @@ impl Callbacks for DynamicRTSCallbacks { }); } + fn after_crate_root_parsing<'tcx>( + &mut self, + _compiler: &interface::Compiler, + queries: &'tcx Queries<'tcx>, + ) -> Compilation { + queries.global_ctxt().unwrap().enter(|tcx| { + // if !excluded(|| tcx.crate_name(LOCAL_CRATE).to_string()) { + { + // Inject #![feature(test)] and #![feature(custom_test_runner)] into the inner crate attributes + let features: &mut Features = unsafe { std::mem::transmute(tcx.features()) }; + + features.declared_lib_features.push((sym::test, DUMMY_SP)); + features + .declared_lang_features + .push((sym::custom_test_frameworks, DUMMY_SP, None)); + + features.declared_features.insert(sym::test); + features + .declared_features + .insert(sym::custom_test_frameworks); + + features.custom_test_frameworks = true; + } + + { + // Add an inner attribute #![test_runner(rustyrts_runner_wrapper)] to the crate attributes + let borrowed = tcx.crate_for_resolver(()).borrow(); + let krate: &mut Crate = unsafe { std::mem::transmute(&borrowed.0) }; + + let generator = &tcx.sess.parse_sess.attr_id_generator; + + { + let style = AttrStyle::Inner; + let path = Path { + span: DUMMY_SP, + segments: vec![PathSegment::from_ident(Ident { + name: sym::test_runner, + span: DUMMY_SP, + })] + .into(), + tokens: None, + }; + + let arg_token = Token::new( + TokenKind::Ident(Symbol::intern("rustyrts_runner_wrapper"), false), + DUMMY_SP, + ); + let arg_tokens = + TokenStream::new(vec![TokenTree::Token(arg_token, Spacing::JointHidden)]); + let delim_args = DelimArgs { + dspan: DelimSpan::dummy(), + delim: Delimiter::Parenthesis, + tokens: arg_tokens, + }; + let attr_args = AttrArgs::Delimited(delim_args); + let span = DUMMY_SP; + + let attr = mk_attr(generator, style, path, attr_args, span); + + krate.attrs.push(attr.clone()); + } + } + // } + }); + + Compilation::Continue + } + fn after_analysis<'compiler, 'tcx>( &mut self, _compiler: &'compiler interface::Compiler, @@ -100,29 +176,23 @@ impl Callbacks for DynamicRTSCallbacks { } /// This function is executed instead of optimized_mir() in the compiler -fn custom_optimized_mir<'tcx>( - tcx: TyCtxt<'tcx>, - def: query_keys::optimized_mir<'tcx>, -) -> query_stored::optimized_mir<'tcx> { - let content = OLD_OPTIMIZED_MIR_PTR.load(SeqCst); +fn custom_optimized_mir<'tcx>(tcx: TyCtxt<'tcx>, key: LocalDefId) -> &'tcx Body<'tcx> { + let content = OLD_OPTIMIZED_MIR.load(SeqCst); // SAFETY: At this address, the original optimized_mir() function has been stored before. // We reinterpret it as a function, while changing the return type to mutable. let orig_function = unsafe { transmute::< usize, - fn( - _: TyCtxt<'tcx>, - _: query_keys::optimized_mir<'tcx>, - ) -> &'tcx mut rustc_middle::mir::Body<'tcx>, // notice the mutable reference here + fn(_: TyCtxt<'tcx>, _: LocalDefId) -> &'tcx mut Body<'tcx>, // notice the mutable reference here >(content) }; - let result = orig_function(tcx, def); + let result = orig_function(tcx, key); if !no_instrumentation(|| tcx.crate_name(LOCAL_CRATE).to_string()) { //############################################################## - // 1. Here the MIR is modified to trace this function at runtime + // 1. Here the MIR is modified to debug this function at runtime modify_body(tcx, result); } @@ -132,27 +202,7 @@ fn custom_optimized_mir<'tcx>( impl DynamicRTSCallbacks { fn run_analysis(&mut self, tcx: TyCtxt) { - //############################################################################################################## - // Collect all MIR bodies that are relevant for code generation - - let code_gen_units = tcx.collect_and_partition_mono_items(()).1; - let bodies = code_gen_units - .iter() - .flat_map(|c| c.items().keys()) - .filter(|m| if let MonoItem::Fn(_) = m { true } else { false }) - .map(|m| { - let MonoItem::Fn(instance) = m else {unreachable!()}; - instance - }) - .filter(|i| tcx.is_mir_available(i.def_id())) - //.filter(|i| i.def_id().is_local()) // It is not feasible to only analyze local MIR - .map(|i| (tcx.optimized_mir(i.def_id()), i.substs)) - .collect_vec(); - - //############################################################################################################## - // Continue at shared analysis - - run_analysis_shared(tcx, bodies); + run_analysis_shared(tcx); } } @@ -176,18 +226,18 @@ fn custom_vtable_entries<'tcx>( for entry in result { if let VtblEntry::Method(instance) = entry { let def_id = instance.def_id(); - - // TODO: it should be feasible to exclude closures here - - let name = def_id_name(tcx, def_id, &[], false, true); // IMPORTANT: no substs here - let checksum = get_checksum_vtbl_entry(tcx, &entry); - trace!("Considering {:?} in checksums of {}", instance, name); - - insert_hashmap( - &mut *NEW_CHECKSUMS_VTBL.get().unwrap().lock().unwrap(), - &name, - checksum, - ) + if !tcx.is_closure(def_id) && !tcx.is_fn_trait(key.def_id()) { + let checksum = get_checksum_vtbl_entry(tcx, &entry); + let name = def_id_name(tcx, def_id, false, true); + + trace!("Considering {:?} in checksums of {}", instance, name); + + insert_hashmap( + &mut *NEW_CHECKSUMS_VTBL.get().unwrap().lock().unwrap(), + &name, + checksum, + ); + } } } } diff --git a/src/dynamic_rts/defid_util.rs b/src/dynamic_rts/defid_util.rs index 1f75d1cc..2e87a7e5 100644 --- a/src/dynamic_rts/defid_util.rs +++ b/src/dynamic_rts/defid_util.rs @@ -88,7 +88,7 @@ pub(crate) fn get_def_id_exported(tcx: TyCtxt, krate: CrateNum, name: &str) -> O }; if let Some(def_id) = maybe_def_id { - let def_path_str = def_id_name(tcx, def_id, &[], false, true); + let def_path_str = def_id_name(tcx, def_id, false, true); if def_path_str.ends_with(name) { return Some(def_id); } diff --git a/src/dynamic_rts/file_loader.rs b/src/dynamic_rts/file_loader.rs index f285d339..5bb34815 100644 --- a/src/dynamic_rts/file_loader.rs +++ b/src/dynamic_rts/file_loader.rs @@ -1,6 +1,6 @@ use rustc_span::source_map::{FileLoader, RealFileLoader}; -use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering::SeqCst; +use std::sync::{atomic::AtomicBool, Arc}; static TEST_RUNNER_INSERTED: AtomicBool = AtomicBool::new(false); static EXTERN_CRATE_INSERTED: AtomicBool = AtomicBool::new(false); @@ -20,17 +20,8 @@ impl FileLoader for TestRunnerFileLoaderProxy { if !TEST_RUNNER_INSERTED.load(SeqCst) { TEST_RUNNER_INSERTED.store(true, SeqCst); - if content.contains("#![feature(custom_test_frameworks)]") { - panic!("Dynamic RustyRTS does not support using a custom test framework. Please use static RustyRTS instead"); - } - - let content = content.replace("#![feature(test)]", ""); let extended_content = format!( - "#![feature(test)] - #![feature(custom_test_frameworks)] - #![test_runner(rustyrts_runner_wrapper)] - - {} + "{} #[allow(unused_extern_crates)] extern crate test as rustyrts_test; @@ -56,6 +47,10 @@ impl FileLoader for TestRunnerFileLoaderProxy { Ok(content) } } + + fn read_binary_file(&self, path: &std::path::Path) -> std::io::Result> { + self.delegate.read_binary_file(path) + } } pub struct InstrumentationFileLoaderProxy { @@ -72,11 +67,6 @@ impl FileLoader for InstrumentationFileLoaderProxy { if !EXTERN_CRATE_INSERTED.load(SeqCst) { EXTERN_CRATE_INSERTED.store(true, SeqCst); - if content.contains("#![feature(custom_test_frameworks)]") { - panic!("Dynamic RustyRTS does not support using a custom test framework. Please use static RustyRTS instead"); - } - - let content = content.replace("#![feature(test)]", ""); let extended_content = format!( "{} @@ -91,4 +81,8 @@ impl FileLoader for InstrumentationFileLoaderProxy { Ok(content) } } + + fn read_binary_file(&self, path: &std::path::Path) -> std::io::Result> { + self.delegate.read_binary_file(path) + } } diff --git a/src/dynamic_rts/instrumentation.rs b/src/dynamic_rts/instrumentation.rs index 3c874fc4..69173784 100644 --- a/src/dynamic_rts/instrumentation.rs +++ b/src/dynamic_rts/instrumentation.rs @@ -1,6 +1,6 @@ use super::mir_util::Traceable; -use crate::callbacks_shared::TEST_MARKER; use crate::names::def_id_name; +use crate::callbacks_shared::TEST_MARKER; use log::trace; use once_cell::sync::OnceCell; use rustc_hir::def_id::DefId; @@ -10,14 +10,14 @@ use rustc_middle::{mir::Body, ty::TyCtxt}; #[cfg(unix)] static ENTRY_FN: OnceCell> = OnceCell::new(); -pub fn modify_body<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { +pub(crate) fn modify_body<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { let def_id = body.source.instance.def_id(); - let outer = def_id_name(tcx, def_id, &[], false, true); + let outer = def_id_name(tcx, def_id, false, true); trace!("Visiting {}", outer); let mut cache_str = None; - let mut cache_u8 = None; + let mut cache_tuple_of_str_and_ptr = None; let mut cache_ret = None; let attrs = &tcx.hir_crate(()).owners[tcx @@ -31,7 +31,7 @@ pub fn modify_body<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { for (_, list) in attrs.iter() { for attr in *list { if attr.name_or_empty().to_ident_string() == TEST_MARKER { - let def_path = def_id_name(tcx, def_id, &[], true, false); + let def_path = def_id_name(tcx, def_id, true, false); let def_path_test = &def_path[0..def_path.len() - 13]; // IMPORTANT: The order in which insert_post, insert_pre are called is critical here @@ -60,7 +60,7 @@ pub fn modify_body<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { } } - body.insert_trace(tcx, &outer, &mut cache_str, &mut cache_u8, &mut cache_ret); + body.insert_trace(tcx, &outer, &mut cache_tuple_of_str_and_ptr, &mut cache_ret); #[cfg(unix)] body.check_calls_to_exit(tcx, &mut cache_ret); @@ -72,3 +72,4 @@ pub fn modify_body<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { } } } + diff --git a/src/dynamic_rts/mir_util.rs b/src/dynamic_rts/mir_util.rs index 4c2699a7..108b015e 100644 --- a/src/dynamic_rts/mir_util.rs +++ b/src/dynamic_rts/mir_util.rs @@ -3,16 +3,23 @@ use std::mem::transmute; use super::defid_util::{get_def_id_post_test_fn, get_def_id_pre_test_fn, get_def_id_trace_fn}; use crate::constants::EDGE_CASES_NO_TRACE; use log::{error, trace}; +use rustc_abi::HasDataLayout; use rustc_abi::{Align, Size}; use rustc_ast::Mutability; +use rustc_const_eval::interpret::CtfeProvenance; +use rustc_data_structures::sorted_map::SortedMap; use rustc_hir::def_id::{DefId, LOCAL_CRATE}; +use rustc_middle::{ + mir::{interpret::AllocId, CallSource, Const, ConstOperand, ConstValue, UnwindAction}, + ty::Region, +}; use rustc_middle::{ mir::{ - interpret::{Allocation, ConstValue, Pointer, Scalar}, - BasicBlock, BasicBlockData, Body, Constant, ConstantKind, Local, LocalDecl, Operand, Place, - ProjectionElem, Rvalue, SourceInfo, Statement, StatementKind, Terminator, TerminatorKind, + interpret::{Allocation, Pointer, Scalar}, + BasicBlock, BasicBlockData, Body, Local, LocalDecl, Operand, Place, ProjectionElem, Rvalue, + SourceInfo, Statement, StatementKind, Terminator, TerminatorKind, }, - ty::{List, RegionKind, Ty, TyCtxt, TyKind, UintTy}, + ty::{List, RegionKind, Ty, TyCtxt, TyKind, TypeAndMut, UintTy}, }; use rustc_span::Span; @@ -24,35 +31,59 @@ use super::defid_util::{get_def_id_post_main_fn, get_def_id_pre_main_fn}; fn insert_local_ret<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> Local { let span = body.span; - let ty_empty = tcx.mk_tup([].iter()); + let ty_empty = tcx.mk_ty_from_kind(TyKind::Tuple(List::empty())); let local_decl_1 = LocalDecl::new(ty_empty, span).immutable(); let local_decls = &mut body.local_decls; let local_1 = local_decls.push(local_decl_1); local_1 } +#[allow(dead_code)] fn insert_local_u8<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> (Local, Ty<'tcx>) { let span = body.span; - let ty_u8 = tcx.mk_ty(TyKind::Uint(UintTy::U8)); - let region = tcx.mk_region(RegionKind::ReErased); - let ty_ref_u8 = tcx.mk_mut_ref(region, ty_u8); + let ty_u8 = tcx.mk_ty_from_kind(TyKind::Uint(UintTy::U8)); + let region = Region::new_from_kind(tcx, RegionKind::ReErased); + let ty_ref_u8 = tcx.mk_ty_from_kind(TyKind::Ref(region, ty_u8, Mutability::Mut)); let local_decl = LocalDecl::new(ty_ref_u8, span).immutable(); let local_decls = &mut body.local_decls; let local = local_decls.push(local_decl); (local, ty_ref_u8) } +#[allow(dead_code)] fn insert_local_str<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> (Local, Ty<'tcx>) { let span = body.span; - let ty_str = tcx.mk_ty(TyKind::Str); - let region = tcx.mk_region(RegionKind::ReErased); - let ty_ref_str = tcx.mk_imm_ref(region, ty_str); + let ty_str = tcx.mk_ty_from_kind(TyKind::Str); + let region = Region::new_from_kind(tcx, RegionKind::ReErased); + let ty_ref_str = tcx.mk_ty_from_kind(TyKind::Ref(region, ty_str, Mutability::Not)); let local_decl = LocalDecl::new(ty_ref_str, span).immutable(); let local_decls = &mut body.local_decls; let local = local_decls.push(local_decl); (local, ty_ref_str) } +#[allow(dead_code)] +fn insert_local_tuple_of_str_and_ptr<'tcx>( + tcx: TyCtxt<'tcx>, + body: &mut Body<'tcx>, +) -> (Local, Ty<'tcx>, Ty<'tcx>, Ty<'tcx>) { + let span = body.span; + let ty_str = tcx.mk_ty_from_kind(TyKind::Str); + let ty_ptr = tcx.mk_ty_from_kind(TyKind::RawPtr(TypeAndMut { + ty: tcx.mk_ty_from_kind(TyKind::Uint(UintTy::U64)), + mutbl: Mutability::Mut, + })); + let region = Region::new_from_kind(tcx, RegionKind::ReErased); + let ty_ref_str = tcx.mk_ty_from_kind(TyKind::Ref(region, ty_str, Mutability::Not)); + let list = tcx.mk_type_list(&[ty_ref_str, ty_ptr]); + let ty_tuple = tcx.mk_ty_from_kind(TyKind::Tuple(list)); + let ty_ref_tuple = tcx.mk_ty_from_kind(TyKind::Ref(region, ty_tuple, Mutability::Mut)); + let local_decl = LocalDecl::new(ty_ref_tuple, span).immutable(); + let local_decls = &mut body.local_decls; + let local = local_decls.push(local_decl); + (local, ty_ref_tuple, ty_ref_str, ty_ptr) +} + //###################################################################################################################### // Functions for inserting assign statements, e.g. (_52 <= const "foo::bar") @@ -71,18 +102,17 @@ fn insert_assign_str<'tcx>( }; let new_allocation = Allocation::from_bytes_byte_aligned_immutable(content.as_bytes()); - let interned_allocation = tcx.intern_const_alloc(new_allocation); + let interned_allocation = tcx.mk_const_alloc(new_allocation); let new_const_value = ConstValue::Slice { data: interned_allocation, - start: 0, - end: content.len(), + meta: content.len() as u64, }; - let new_literal = ConstantKind::Val(new_const_value, ty_ref_str); + let new_literal = Const::Val(new_const_value, ty_ref_str); - let new_constant = Constant { + let new_constant = ConstOperand { span, user_ty: None, - literal: new_literal, + const_: new_literal, }; let new_operand = Operand::Constant(Box::new(new_constant)); @@ -104,6 +134,7 @@ fn insert_assign_str<'tcx>( (const_assign_statement_str, place_ref_str) } +#[allow(dead_code)] fn insert_assign_u8<'tcx>( tcx: TyCtxt<'tcx>, local_u8: Local, @@ -119,20 +150,20 @@ fn insert_assign_u8<'tcx>( }; let content = [content]; - let new_allocation = - Allocation::from_bytes(&content[..], Align::from_bytes(1).unwrap(), Mutability::Mut); - let interned_allocation = tcx.intern_const_alloc(new_allocation); - let memory_allocation = tcx.create_memory_alloc(interned_allocation); + let new_allocation = Allocation::from_bytes(&content[..], Align::ONE, Mutability::Mut); + let interned_allocation = tcx.mk_const_alloc(new_allocation); + let memory_allocation = tcx.reserve_and_set_memory_alloc(interned_allocation); + let provenance = CtfeProvenance::from(memory_allocation); - let new_ptr = Pointer::new(memory_allocation, Size::ZERO); + let new_ptr = Pointer::new(provenance, Size::ZERO); let new_const_value = ConstValue::Scalar(Scalar::from_pointer(new_ptr, &tcx)); - let new_literal = ConstantKind::Val(new_const_value, ty_ref_u8); + let new_literal = Const::Val(new_const_value, ty_ref_u8); - let new_constant = Constant { + let new_constant = ConstOperand { span, user_ty: None, - literal: new_literal, + const_: new_literal, }; let new_operand = Operand::Constant(Box::new(new_constant)); @@ -154,6 +185,109 @@ fn insert_assign_u8<'tcx>( (const_assign_statement_u8, place_ref_u8) } +fn insert_assign_tuple_of_str_and_ptr<'tcx>( + tcx: TyCtxt<'tcx>, + local_tuple_of_str_and_ptr: Local, + content_str: &str, + content_ptr: u64, + ty_tuple_of_str_and_ptr: Ty<'tcx>, + span: Span, +) -> (Statement<'tcx>, Place<'tcx>) { + // let const_assign_statements = { + let const_assign_statement = { + let place_str = Place { + local: local_tuple_of_str_and_ptr, + projection: tcx.mk_place_elems(&[]), + }; + + let str_allocation = Allocation::from_bytes_byte_aligned_immutable(content_str.as_bytes()); + let str_interned_allocation = tcx.mk_const_alloc(str_allocation); + let str_memory_alloc = tcx.reserve_and_set_memory_alloc(str_interned_allocation); + + let tuple_allocation = Allocation::from_bytes( + [ + [0x0; 8], + content_str.len().to_ne_bytes(), + content_ptr.to_ne_bytes(), + ] + .concat(), + tcx.data_layout().aggregate_align.pref, + Mutability::Mut, + ); + + //let init_mask: &mut InitMask = std::mem::transmute(tuple_allocation.init_mask()); + + let provenance_map = tuple_allocation.provenance(); + let map: &mut SortedMap = + unsafe { std::mem::transmute(provenance_map.ptrs()) }; + map.insert(Size::from_bytes(0), str_memory_alloc); + + let tuple_interned_allocation = tcx.mk_const_alloc(tuple_allocation); + + let tuple_memory_allocation = tcx.reserve_and_set_memory_alloc(tuple_interned_allocation); + let provenance = CtfeProvenance::from(tuple_memory_allocation); + + let tuple_ptr = Pointer::new(provenance, Size::ZERO); + let tuple_const_value = ConstValue::Scalar(Scalar::from_pointer(tuple_ptr, &tcx)); + + let ref_tuple = Const::Val(tuple_const_value, ty_tuple_of_str_and_ptr); + + let tuple_constant = ConstOperand { + span, + user_ty: None, + const_: ref_tuple, + }; + + let new_operand = Operand::Constant(Box::new(tuple_constant)); + let new_rvalue = Rvalue::Use(new_operand); + + let const_assign_statement = Statement { + source_info: SourceInfo::outermost(span), + kind: StatementKind::Assign(Box::new((place_str, new_rvalue))), + }; + + const_assign_statement + }; + + // let const_assign_statement_ptr = { + // let place_u8 = Place { + // local: local_tuple_of_str_and_ptr, + // projection: tcx + // .mk_place_elems([ProjectionElem::Field(Field::from_usize(1), ty_ptr)].iter()), + // }; + + // let new_const_value = ConstValue::Scalar(Scalar::null_ptr(&tcx)); + + // let new_literal = ConstantKind::Val(new_const_value, ty_ptr); + + // let new_constant = Constant { + // span, + // user_ty: None, + // literal: new_literal, + // }; + + // let new_operand = Operand::Constant(Box::new(new_constant)); + // let new_rvalue = Rvalue::Use(new_operand); + + // let const_assign_statement = Statement { + // source_info: SourceInfo::outermost(span), + // kind: StatementKind::Assign(Box::new((place_u8, new_rvalue))), + // }; + + // const_assign_statement + // }; + + // [const_assign_statement_ptr, const_assign_statement_str] + // }; + + let place_ref_tuple_of_str_and_ptr = Place { + local: local_tuple_of_str_and_ptr, + projection: tcx.mk_place_elems(&[]), + }; + + (const_assign_statement, place_ref_tuple_of_str_and_ptr) +} + fn create_call<'tcx>( tcx: TyCtxt<'tcx>, def_id: DefId, @@ -163,14 +297,14 @@ fn create_call<'tcx>( place_elem_list: &'tcx List>>, target: Option, ) -> Terminator<'tcx> { - let func_subst = tcx.mk_substs([].iter()); - let func_ty = tcx.mk_ty(TyKind::FnDef(def_id, func_subst)); - let literal = ConstantKind::Val(ConstValue::ZeroSized, func_ty); + let func_subst = tcx.mk_args(&[]); + let func_ty = tcx.mk_ty_from_kind(TyKind::FnDef(def_id, func_subst)); + let literal = Const::Val(ConstValue::ZeroSized, func_ty); - let func_constant = Constant { + let func_constant = ConstOperand { span, user_ty: None, - literal: literal, + const_: literal, }; let func_operand = Operand::Constant(Box::new(func_constant)); @@ -183,10 +317,10 @@ fn create_call<'tcx>( func: func_operand, args: args_vec, destination: place_ret, - target: target, - cleanup: None, - from_hir_call: false, + target, + call_source: CallSource::Normal, fn_span: span, + unwind: UnwindAction::Continue, }; let terminator = Terminator { @@ -205,8 +339,7 @@ pub trait Traceable<'tcx> { &mut self, tcx: TyCtxt<'tcx>, name: &str, - cache_str: &mut Option<(Local, Ty<'tcx>)>, - cache_u8: &mut Option<(Local, Ty<'tcx>)>, + cache_tuple_of_str_and_ptr: &mut Option<(Local, Ty<'tcx>, Ty<'tcx>, Ty<'tcx>)>, cache_ret: &mut Option, ); @@ -241,8 +374,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { &mut self, tcx: TyCtxt<'tcx>, name: &str, - cache_str: &mut Option<(Local, Ty<'tcx>)>, - cache_u8: &mut Option<(Local, Ty<'tcx>)>, + cache_tuple_of_str_and_ptr: &mut Option<(Local, Ty<'tcx>, Ty<'tcx>, Ty<'tcx>)>, cache_ret: &mut Option, ) { if !EDGE_CASES_NO_TRACE.iter().any(|c| name.ends_with(c)) { @@ -258,22 +390,26 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { }; let local_ret = *cache_ret.get_or_insert_with(|| insert_local_ret(tcx, self)); - let (local_str, ty_ref_str) = - *cache_str.get_or_insert_with(|| insert_local_str(tcx, self)); - let (local_u8, ty_ref_u8) = *cache_u8.get_or_insert_with(|| insert_local_u8(tcx, self)); + let (local_tuple_of_str_and_ptr, ty_tuple_of_str_and_ptr, _ty_ref_str, _ty_ptr) = + *cache_tuple_of_str_and_ptr + .get_or_insert_with(|| insert_local_tuple_of_str_and_ptr(tcx, self)); let span = self.span; //******************************************************* // Create assign statements - let place_elem_list = tcx.mk_place_elems([].iter()); - - let (assign_statement_str, place_ref_str) = - insert_assign_str(tcx, local_str, place_elem_list, name, ty_ref_str, span); + let place_elem_list = tcx.mk_place_elems(&[]); - let (assign_statement_u8, place_ref_u8) = - insert_assign_u8(tcx, local_u8, place_elem_list, 0u8, ty_ref_u8, span); + let (assign_statement, place_ref_tuple_of_str_and_ptr) = + insert_assign_tuple_of_str_and_ptr( + tcx, + local_tuple_of_str_and_ptr, + name, + u64::MAX, + ty_tuple_of_str_and_ptr, + span, + ); //******************************************************* // Create new basic block @@ -281,8 +417,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let basic_blocks = self.basic_blocks.as_mut(); let mut args_vec = Vec::new(); - args_vec.push(Operand::Move(place_ref_str)); - args_vec.push(Operand::Move(place_ref_u8)); + args_vec.push(Operand::Move(place_ref_tuple_of_str_and_ptr)); let terminator = create_call( tcx, def_id_trace_fn, @@ -294,8 +429,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { ); let mut new_basic_block_data = BasicBlockData::new(Some(terminator)); - new_basic_block_data.statements.push(assign_statement_str); - new_basic_block_data.statements.push(assign_statement_u8); + new_basic_block_data.statements.push(assign_statement); let index = basic_blocks.push(new_basic_block_data); @@ -316,7 +450,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let span = self.span; - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); //******************************************************* // Create new basic block @@ -357,7 +491,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let span = self.span; - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); //******************************************************* // Create new basic block @@ -406,7 +540,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { unsafe { transmute(&self.basic_blocks.get(BasicBlock::from_usize(i)).unwrap().terminator().kind) }; match terminator_kind { - TerminatorKind::Return | TerminatorKind::Resume => { + TerminatorKind::Return | TerminatorKind::UnwindResume => { cache_str.get_or_insert_with(|| insert_local_str(tcx, self)); cache_ret.get_or_insert_with(|| insert_local_ret(tcx, self)); @@ -418,7 +552,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { //******************************************************* // Create assign statements - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); let (assign_statement_str, place_ref_str) = insert_assign_str(tcx, local_str, place_elem_list, name, ty_ref_str, span); @@ -442,7 +576,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let mut new_basic_block_data = BasicBlockData::new(Some(terminator)); new_basic_block_data.statements.push(assign_statement_str); - if let TerminatorKind::Resume = terminator_kind { + if let TerminatorKind::UnwindResume = terminator_kind { new_basic_block_data.is_cleanup = true; } @@ -451,10 +585,10 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { // Swap bb_i and the new basic block basic_blocks.swap(BasicBlock::from_usize(i), index); } - TerminatorKind::Call { cleanup, .. } - | TerminatorKind::Assert { cleanup, .. } - | TerminatorKind::InlineAsm { cleanup, .. } - if cleanup.is_none() + TerminatorKind::Call { unwind, .. } + | TerminatorKind::Assert { unwind, .. } + | TerminatorKind::InlineAsm { unwind, .. } + if *unwind == UnwindAction::Continue && !self .basic_blocks .get(BasicBlock::from_usize(i)) @@ -462,7 +596,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { .is_cleanup => { if let Some(call_bb) = cache_call { - cleanup.replace(*call_bb); + *unwind = UnwindAction::Cleanup(*call_bb); } else { cache_str.get_or_insert_with(|| insert_local_str(tcx, self)); cache_ret.get_or_insert_with(|| insert_local_ret(tcx, self)); @@ -475,7 +609,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let basic_blocks = self.basic_blocks.as_mut(); // At this index, we will insert the call to rustyrts_post_test() - cleanup.replace(basic_blocks.next_index()); + *unwind = UnwindAction::Cleanup(basic_blocks.next_index()); //******************************************************* // Insert new bb to resume unwinding @@ -483,7 +617,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let resume_bb = { let terminator = Terminator { source_info: SourceInfo::outermost(span), - kind: TerminatorKind::Resume, + kind: TerminatorKind::UnwindResume, }; let mut new_basic_block_data = BasicBlockData::new(Some(terminator)); @@ -495,7 +629,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { //******************************************************* // Create assign statements - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); let (assign_statement_str, place_ref_str) = insert_assign_str( tcx, local_str, @@ -566,8 +700,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { if let TerminatorKind::Call { func, .. } = terminator_kind { if let Operand::Constant(boxed_def_id) = func { - if let ConstantKind::Val(ConstValue::ZeroSized, func_ty) = boxed_def_id.literal - { + if let Const::Val(ConstValue::ZeroSized, func_ty) = boxed_def_id.const_ { if let TyKind::FnDef(def_id, _) = func_ty.kind() { if *def_id == def_id_exit { // We found a call to std::process::exit() @@ -578,7 +711,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let span = self.span; - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); //******************************************************* // Create new basic block @@ -622,6 +755,8 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { cache_ret: &mut Option, cache_call: &mut Option, ) { + use crate::names::def_id_name; + trace!("Inserting post_main() into {:?}", self.source.def_id()); let Some(def_id_post_fn) = get_def_id_post_main_fn(tcx) else { @@ -641,14 +776,14 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { .kind) }; match terminator_kind { - TerminatorKind::Return | TerminatorKind::Resume => { + TerminatorKind::Return | TerminatorKind::UnwindResume => { cache_ret.get_or_insert_with(|| insert_local_ret(tcx, self)); let local_ret = cache_ret.unwrap(); let span = self.span; - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); //******************************************************* // Create new basic block @@ -668,7 +803,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let mut new_basic_block_data = BasicBlockData::new(Some(terminator)); - if let TerminatorKind::Resume = terminator_kind { + if let TerminatorKind::UnwindResume = terminator_kind { new_basic_block_data.is_cleanup = true; } @@ -677,10 +812,10 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { // Swap bb_i and the new basic block basic_blocks.swap(BasicBlock::from_usize(i), index); } - TerminatorKind::Call { cleanup, .. } - | TerminatorKind::Assert { cleanup, .. } - | TerminatorKind::InlineAsm { cleanup, .. } - if cleanup.is_none() + TerminatorKind::Call { unwind, .. } + | TerminatorKind::Assert { unwind, .. } + | TerminatorKind::InlineAsm { unwind, .. } + if *unwind == UnwindAction::Continue && !self .basic_blocks .get(BasicBlock::from_usize(i)) @@ -694,17 +829,27 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { .terminator() .kind; - if let TerminatorKind::Call { destination, .. } = terminator_kind { + if let TerminatorKind::Call { + destination, func, .. + } = terminator_kind + { if let Some(local) = destination.as_local() { if local.as_usize() == 0 { // LLVM terminates with an error when calls that directly return something from main() are extended with a Resume terminator continue; } } + + // HACK: in tracing, injecting a cleanup into a particular terminator results in an LLVM error + if func.const_fn_def().is_some_and(|(def, _)| { + def_id_name(tcx, def, false, true).contains("__is_enabled") + }) { + continue; + } } if let Some(call_bb) = cache_call { - cleanup.replace(*call_bb); + *unwind = UnwindAction::Cleanup(*call_bb); } else { cache_ret.get_or_insert_with(|| insert_local_ret(tcx, self)); @@ -715,7 +860,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let basic_blocks = self.basic_blocks.as_mut(); // At this index, we will insert the call to rustyrts_post_main() - cleanup.replace(basic_blocks.next_index()); + *unwind = UnwindAction::Cleanup(basic_blocks.next_index()); //******************************************************* // Insert new bb to resume @@ -723,7 +868,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { let resume_bb = { let terminator = Terminator { source_info: SourceInfo::outermost(span), - kind: TerminatorKind::Resume, + kind: TerminatorKind::UnwindResume, }; let mut new_basic_block_data = BasicBlockData::new(Some(terminator)); @@ -735,7 +880,7 @@ impl<'tcx> Traceable<'tcx> for Body<'tcx> { //******************************************************* // Create new basic block - let place_elem_list = tcx.mk_place_elems([].iter()); + let place_elem_list = tcx.mk_place_elems(&[]); let args_vec = Vec::with_capacity(0); let terminator = create_call( diff --git a/src/dynamic_rts/mod.rs b/src/dynamic_rts/mod.rs new file mode 100644 index 00000000..4cd55213 --- /dev/null +++ b/src/dynamic_rts/mod.rs @@ -0,0 +1,5 @@ +pub mod callback; +pub mod defid_util; +pub mod file_loader; +pub mod instrumentation; +pub mod mir_util; diff --git a/src/format.rs b/src/format.rs index 7c531eeb..27f35a9b 100644 --- a/src/format.rs +++ b/src/format.rs @@ -9,8 +9,7 @@ use std::io::Write; use crate::constants::ENV_RUSTYRTS_LOG; pub fn create_logger<'a>() -> Builder { - let mut builder = - env_logger::Builder::from_env(Env::from(ENV_RUSTYRTS_LOG).default_filter_or("info")); + let mut builder = env_logger::Builder::from_env(Env::new().filter_or(ENV_RUSTYRTS_LOG, "info")); builder.format(colored_record); diff --git a/src/fs_utils.rs b/src/fs_utils.rs index 7bde6d04..1e44fccf 100644 --- a/src/fs_utils.rs +++ b/src/fs_utils.rs @@ -7,8 +7,8 @@ use std::io::Write; use std::path::PathBuf; use crate::constants::{ - ENDING_CHANGES, ENDING_CHECKSUM, ENDING_CHECKSUM_CONST, ENDING_CHECKSUM_VTBL, ENDING_GRAPH, - ENDING_TEST, ENDING_TRACE, ENV_TARGET_DIR, + ENDING_CHANGES, ENDING_CHECKSUM, ENDING_CHECKSUM_CONST, ENDING_CHECKSUM_VTBL, + ENDING_DEPENDENCIES, ENDING_GRAPH, ENDING_TEST, ENDING_TRACE, ENV_TARGET_DIR, }; #[cfg(unix)] @@ -21,7 +21,8 @@ pub fn get_target_dir(mode: &str) -> PathBuf { } pub fn get_target_dir_relative(mode: &str) -> PathBuf { - PathBuf::from(std::env::var(ENV_TARGET_DIR).unwrap_or(format!("target_{}", mode).to_string())) + let path = std::env::var(ENV_TARGET_DIR).unwrap_or(format!("target_{}", mode).to_string()); + PathBuf::from(path) } pub fn get_static_path(absolute: bool) -> PathBuf { @@ -80,6 +81,11 @@ pub fn get_checksums_const_path(mut path_buf: PathBuf, crate_name: &str, id: u64 path_buf } +pub fn get_dependencies_path(mut path_buf: PathBuf, test_name: &str) -> PathBuf { + path_buf.push(format!("{}{}", test_name, ENDING_DEPENDENCIES)); + path_buf +} + pub fn get_traces_path(mut path_buf: PathBuf, test_name: &str) -> PathBuf { path_buf.push(format!("{}{}", test_name, ENDING_TRACE)); path_buf diff --git a/src/info.rs b/src/info.rs index 2dd08596..b1040ef4 100644 --- a/src/info.rs +++ b/src/info.rs @@ -180,14 +180,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " cf_protection: {:?}", config.opts.unstable_opts.cf_protection ); - info!( - " cgu_partitioning_strategy: {:?}", - config.opts.unstable_opts.cgu_partitioning_strategy - ); - info!( - " codegen_backend: {:?}", - config.opts.unstable_opts.codegen_backend - ); info!( " combine_cgu: {:?}", config.opts.unstable_opts.combine_cgu @@ -209,20 +201,10 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " dep_info_omit_d_target: {:?}", config.opts.unstable_opts.dep_info_omit_d_target ); - info!(" dep_tasks: {:?}", config.opts.unstable_opts.dep_tasks); - info!( - " diagnostic_width: {:?}", - config.opts.unstable_opts.diagnostic_width - ); - info!(" dlltool: {:?}", config.opts.unstable_opts.dlltool); info!( " dont_buffer_diagnostics: {:?}", config.opts.unstable_opts.dont_buffer_diagnostics ); - info!( - " drop_tracking: {:?}", - config.opts.unstable_opts.drop_tracking - ); info!( " dual_proc_macros: {:?}", config.opts.unstable_opts.dual_proc_macros @@ -231,10 +213,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " dump_dep_graph: {:?}", config.opts.unstable_opts.dump_dep_graph ); - info!( - " dump_drop_tracking_cfg: {:?}", - config.opts.unstable_opts.dump_drop_tracking_cfg - ); info!(" dump_mir: {:?}", config.opts.unstable_opts.dump_mir); info!( " dump_mir_dataflow: {:?}", @@ -327,10 +305,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " incremental_info: {:?}", config.opts.unstable_opts.incremental_info ); - info!( - " incremental_relative_spans: {:?}", - config.opts.unstable_opts.incremental_relative_spans - ); info!( " incremental_verify_ich: {:?}", config.opts.unstable_opts.incremental_verify_ich @@ -356,18 +330,10 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " input_stats: {:?}", config.opts.unstable_opts.input_stats ); - info!( - " instrument_coverage: {:?}", - config.opts.unstable_opts.instrument_coverage - ); info!( " instrument_mcount: {:?}", config.opts.unstable_opts.instrument_mcount ); - info!( - " keep_hygiene_data: {:?}", - config.opts.unstable_opts.keep_hygiene_data - ); info!( " layout_seed: {:?}", config.opts.unstable_opts.layout_seed @@ -415,10 +381,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " mir_opt_level: {:?}", config.opts.unstable_opts.mir_opt_level ); - info!( - " mir_pretty_relative_line_numbers: {:?}", - config.opts.unstable_opts.mir_pretty_relative_line_numbers - ); info!( " move_size_limit: {:?}", config.opts.unstable_opts.move_size_limit @@ -484,7 +446,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { config.opts.unstable_opts.panic_in_drop ); info!(" parse_only: {:?}", config.opts.unstable_opts.parse_only); - info!(" perf_stats: {:?}", config.opts.unstable_opts.perf_stats); info!(" plt: {:?}", config.opts.unstable_opts.plt); info!(" polonius: {:?}", config.opts.unstable_opts.polonius); info!( @@ -624,11 +585,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " strict_init_checks: {:?}", config.opts.unstable_opts.strict_init_checks ); - info!(" strip: {:?}", config.opts.unstable_opts.strip); - info!( - " symbol_mangling_version: {:?}", - config.opts.unstable_opts.symbol_mangling_version - ); info!(" teach: {:?}", config.opts.unstable_opts.teach); info!(" temps_dir: {:?}", config.opts.unstable_opts.temps_dir); info!(" thinlto: {:?}", config.opts.unstable_opts.thinlto); @@ -654,10 +610,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " track_diagnostics: {:?}", config.opts.unstable_opts.track_diagnostics ); - info!( - " trait_solver: {:?}", - config.opts.unstable_opts.trait_solver - ); info!( " translate_additional_ftl: {:?}", config.opts.unstable_opts.translate_additional_ftl @@ -716,7 +668,6 @@ pub(crate) fn print_compiler_config(config: &mut interface::Config) { " validate_mir: {:?}", config.opts.unstable_opts.validate_mir ); - info!(" verbose: {:?}", config.opts.unstable_opts.verbose); info!( " verify_llvm_ir: {:?}", config.opts.unstable_opts.verify_llvm_ir diff --git a/src/lib.rs b/src/lib.rs index 9baf73d6..8da29e49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,27 +17,16 @@ extern crate rustc_session; // required for analyzing and modifying the MIR extern crate rustc_abi; extern crate rustc_ast; +extern crate rustc_attr; +extern crate rustc_const_eval; +extern crate rustc_feature; extern crate rustc_hir; extern crate rustc_middle; extern crate rustc_span; +extern crate rustc_type_ir; -// required for running compiler on strings during testing -extern crate rustc_error_codes; -extern crate rustc_errors; - -pub mod static_rts { - pub mod callback; - pub mod graph; - pub mod visitor; -} - -pub mod dynamic_rts { - pub mod callback; - pub mod defid_util; - pub mod file_loader; - pub mod instrumentation; - pub mod mir_util; -} +pub mod static_rts; +pub mod dynamic_rts; pub mod callbacks_shared; pub mod checksums; @@ -47,5 +36,3 @@ pub mod format; pub mod fs_utils; pub mod info; pub mod names; - -pub mod utils; diff --git a/src/names.rs b/src/names.rs index 4f1a1b93..f5d6b81d 100644 --- a/src/names.rs +++ b/src/names.rs @@ -1,9 +1,13 @@ use lazy_static::lazy_static; use regex::Regex; -use rustc_hir::{def_id::DefId, definitions::DefPathData}; -use rustc_middle::ty::print::Printer; -use rustc_middle::ty::{print::FmtPrinter, GenericArg, TyCtxt}; -use rustc_resolve::Namespace; +use rustc_data_structures::stable_hasher::ToStableHashKey; +use rustc_hir::{def::Namespace, def_id::DefId, definitions::DefPathData}; +use rustc_middle::ty::{ + print::FmtPrinter, AliasTy, Binder, FnSig, GenericArgs, ParamTy, Ty, TyCtxt, TypeAndMut, +}; +use rustc_middle::ty::{print::Printer, List}; +use rustc_span::{def_id::LOCAL_CRATE, Symbol}; +use rustc_type_ir::TyKind; lazy_static! { static ref RE_LIFETIME: [Regex; 2] = [ @@ -12,33 +16,40 @@ lazy_static! { ]; } -#[cfg(feature = "monomorphize")] -lazy_static! { - static ref RE_CLOSURE: Regex = Regex::new(r"\[closure@.*?\/.*?\]").unwrap(); +/// Custom naming scheme for MIR bodies, adapted from def_path_debug_str() in TyCtxt +pub(crate) fn def_id_name<'tcx>( + tcx: TyCtxt<'tcx>, + def_id: DefId, + add_crate_id: bool, + trimmed: bool, +) -> String { + mono_def_id_name(tcx, def_id, List::empty(), add_crate_id, trimmed) } /// Custom naming scheme for MIR bodies, adapted from def_path_debug_str() in TyCtxt -pub(crate) fn def_id_name<'tcx>( +pub(crate) fn mono_def_id_name<'tcx>( tcx: TyCtxt<'tcx>, def_id: DefId, - substs: &'tcx [GenericArg<'tcx>], + substs: &'tcx GenericArgs<'tcx>, add_crate_id: bool, trimmed: bool, ) -> String { assert!(trimmed | def_id.is_local()); + let substs = filter_generic_args(tcx, substs, 1); + let crate_id = if add_crate_id { if def_id.is_local() { format!( "[{:04x}]::", - tcx.sess.local_stable_crate_id().to_u64() >> (8 * 6) + tcx.stable_crate_id(LOCAL_CRATE).as_u64() >> (8 * 6) ) } else { let cstore = tcx.cstore_untracked(); format!( "[{:04x}]::", - cstore.stable_crate_id(def_id.krate).to_u64() >> (8 * 6) + cstore.stable_crate_id(def_id.krate).as_u64() >> (8 * 6) ) } } else { @@ -63,13 +74,6 @@ pub(crate) fn def_id_name<'tcx>( def_path_str = re.replace_all(&def_path_str, "${3}").to_string(); } - #[cfg(feature = "monomorphize")] - { - def_path_str = RE_CLOSURE - .replace_all(&def_path_str, "[closure]") - .to_string(); - } - // Occasionally, there is a newline which we do not want to keep def_path_str = def_path_str.replace("\n", ""); @@ -79,38 +83,37 @@ pub(crate) fn def_id_name<'tcx>( pub fn def_path_str_with_substs_with_no_visible_path<'t>( tcx: TyCtxt<'t>, def_id: DefId, - substs: &'t [GenericArg<'t>], + substs: &'t GenericArgs<'t>, trimmed: bool, ) -> String { let ns = guess_def_namespace(tcx, def_id); + let mut printer = FmtPrinter::new(tcx, ns); + if trimmed { rustc_middle::ty::print::with_forced_trimmed_paths!( - rustc_middle::ty::print::with_no_visible_paths!( - FmtPrinter::new(tcx, ns).print_def_path(def_id, substs) - ) + rustc_middle::ty::print::with_no_visible_paths!(printer.print_def_path(def_id, substs)) ) } else { - rustc_middle::ty::print::with_no_visible_paths!( - FmtPrinter::new(tcx, ns).print_def_path(def_id, substs) - ) + rustc_middle::ty::print::with_no_visible_paths!(printer.print_def_path(def_id, substs)) } - .unwrap() - .into_buffer() + .unwrap(); + + printer.into_buffer() } -// Source: https://doc.rust-lang.org/stable/nightly-rustc/src/rustc_middle/ty/print/pretty.rs.html#1766 +// Source: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_middle/ty/print/pretty.rs.html#1803 // HACK(eddyb) get rid of `def_path_str` and/or pass `Namespace` explicitly always // (but also some things just print a `DefId` generally so maybe we need this?) fn guess_def_namespace(tcx: TyCtxt<'_>, def_id: DefId) -> Namespace { match tcx.def_key(def_id).disambiguated_data.data { - DefPathData::TypeNs(..) | DefPathData::CrateRoot | DefPathData::ImplTrait => { + DefPathData::TypeNs(..) | DefPathData::CrateRoot | DefPathData::OpaqueTy => { Namespace::TypeNS } DefPathData::ValueNs(..) | DefPathData::AnonConst - | DefPathData::ClosureExpr + | DefPathData::Closure | DefPathData::Ctor => Namespace::ValueNS, DefPathData::MacroNs(..) => Namespace::MacroNS, @@ -118,3 +121,139 @@ fn guess_def_namespace(tcx: TyCtxt<'_>, def_id: DefId) -> Namespace { _ => Namespace::TypeNS, } } + +#[allow(dead_code)] +fn filter_generic_args<'tcx>( + tcx: TyCtxt<'tcx>, + args: &'tcx GenericArgs<'tcx>, + depth: usize, +) -> &'tcx GenericArgs<'tcx> { + let mut new_args = Vec::new(); + + for arg in args.into_iter() { + let new_arg = if let Some(ty) = arg.as_type() { + filter_ty(tcx, ty, depth).into() + } else { + arg + }; + new_args.push(new_arg); + } + + tcx.mk_args(&new_args) +} + +fn get_placeholder<'tcx>(tcx: TyCtxt<'tcx>, content: &str) -> Ty<'tcx> { + let param_ty = ParamTy::new(0, Symbol::intern(content)); + tcx.mk_ty_from_kind(TyKind::Param(param_ty)) +} + +const MAX_MONOMORPHIZATION_DEPTH: usize = 4; + +fn filter_ty<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, depth: usize) -> Ty<'tcx> { + if depth > MAX_MONOMORPHIZATION_DEPTH { + return get_placeholder(tcx, ".."); + } + + let kind = match ty.kind() { + TyKind::Bool => { + return ty; + } + TyKind::Char => { + return ty; + } + TyKind::Int(_int_ty) => { + return ty; + } + TyKind::Uint(_uint_ty) => { + return ty; + } + TyKind::Float(_float_ty) => { + return ty; + } + TyKind::Adt(def, args) => TyKind::Adt(*def, filter_generic_args(tcx, args, depth + 1)), + TyKind::Foreign(_def_id) => { + return ty; + } + TyKind::Str => { + return ty; + } + TyKind::Array(ty, const_) => TyKind::Array(filter_ty(tcx, *ty, depth + 1), *const_), + TyKind::Slice(ty) => TyKind::Slice(filter_ty(tcx, *ty, depth + 1)), + TyKind::RawPtr(TypeAndMut { ty, mutbl }) => TyKind::RawPtr(TypeAndMut { + ty: filter_ty(tcx, *ty, depth + 1), + mutbl: *mutbl, + }), + TyKind::Ref(region, ty, mutbl) => { + TyKind::Ref(*region, filter_ty(tcx, *ty, depth + 1), *mutbl) + } + TyKind::FnDef(def_id, args) => { + TyKind::FnDef(*def_id, filter_generic_args(tcx, *args, depth + 1)) + } + TyKind::FnPtr(poly_fn_sig) => { + let bound = poly_fn_sig.bound_vars(); + let sig = poly_fn_sig.skip_binder(); + let list = filter_tys(tcx, sig.inputs_and_output, depth + 1); + let new_inputs_and_outputs = tcx.mk_type_list(&list); + let new_sig = FnSig { + inputs_and_output: new_inputs_and_outputs, + c_variadic: sig.c_variadic, + unsafety: sig.unsafety, + abi: sig.abi, + }; + TyKind::FnPtr(Binder::bind_with_vars(new_sig, bound)) + } + TyKind::Dynamic(_predicates, _region, _kind) => { + return ty; + } // TODO + TyKind::Closure(def_id, _args) => { + // TyKind::Closure(*def_id, filter_generic_args(tcx, *def_id, *args)) + let name = format!("Fn#{}",tcx.with_stable_hashing_context(|hasher| def_id.to_stable_hash_key(&hasher)).0); + return get_placeholder(tcx, &name); + } + TyKind::Coroutine(def_id, _args, _mvblt) => { + // TyKind::Coroutine(*def_id, filter_generic_args(tcx, *def_id, *args), *mvblt) + let name = format!("Fn#{}",tcx.with_stable_hashing_context(|hasher| def_id.to_stable_hash_key(&hasher)).0); + return get_placeholder(tcx, &name); + } + TyKind::CoroutineWitness(def_id, args) => { + TyKind::CoroutineWitness(*def_id, filter_generic_args(tcx, *args, depth + 1)) + } + TyKind::Never => { + return ty; + } + TyKind::Tuple(tys) => { + let list = filter_tys(tcx, tys, depth + 1); + TyKind::Tuple(tcx.mk_type_list(&list)) + } + TyKind::Alias(kind, alias_ty) => { + let new_alias_ty = AliasTy::new( + tcx, + alias_ty.def_id, + filter_generic_args(tcx, alias_ty.args, depth + 1), + ); + TyKind::Alias(*kind, new_alias_ty) + } + TyKind::Param(_param_ty) => { + return ty; + } + TyKind::Bound(_idx, _bound_ty) => { + return ty; + } + TyKind::Placeholder(_placeholder_ty) => { + return ty; + } + TyKind::Infer(_infer_ty) => { + return ty; + } + TyKind::Error(_error_ty) => { + return ty; + } + }; + tcx.mk_ty_from_kind(kind) +} + +fn filter_tys<'tcx>(tcx: TyCtxt<'tcx>, tys: &'tcx List>, depth: usize) -> Vec> { + tys.into_iter() + .map(|ty| filter_ty(tcx, ty, depth)) + .collect() +} diff --git a/src/static_rts/callback.rs b/src/static_rts/callback.rs index 8c6492f8..49d437d3 100644 --- a/src/static_rts/callback.rs +++ b/src/static_rts/callback.rs @@ -1,34 +1,33 @@ use std::mem::transmute; -use std::path::PathBuf; use std::sync::Mutex; -use crate::callbacks_shared::{ - excluded, run_analysis_shared, EXCLUDED, NEW_CHECKSUMS, NEW_CHECKSUMS_CONST, - NEW_CHECKSUMS_VTBL, OLD_VTABLE_ENTRIES, +use crate::{constants::SUFFIX_DYN, static_rts::visitor::{create_dependency_graph, MonoItemCollectionMode}}; +use crate::{ + callbacks_shared::{ + excluded, run_analysis_shared, EXCLUDED, NEW_CHECKSUMS, NEW_CHECKSUMS_CONST, + NEW_CHECKSUMS_VTBL, OLD_VTABLE_ENTRIES, PATH_BUF, + }, + fs_utils::get_graph_path, }; -use crate::constants::SUFFIX_DYN; -use itertools::Itertools; -use log::trace; -use once_cell::sync::OnceCell; +use log::{debug, trace}; use rustc_driver::{Callbacks, Compilation}; use rustc_hir::def_id::LOCAL_CRATE; use rustc_interface::{interface, Queries}; -use rustc_middle::mir::mono::MonoItem; -use rustc_middle::ty::{List, PolyTraitRef, TyCtxt, VtblEntry}; +use rustc_middle::ty::{PolyTraitRef, TyCtxt, VtblEntry}; use rustc_session::config::CrateType; use std::sync::atomic::Ordering::SeqCst; use crate::checksums::{get_checksum_vtbl_entry, insert_hashmap, Checksums}; -use crate::fs_utils::{get_graph_path, get_static_path, write_to_file}; +use crate::fs_utils::{get_static_path, write_to_file}; use crate::names::def_id_name; -use super::graph::DependencyGraph; -use super::visitor::GraphVisitor; +pub struct StaticRTSCallbacks {} -pub(crate) static PATH_BUF: OnceCell = OnceCell::new(); - -pub struct StaticRTSCallbacks { - graph: DependencyGraph, +impl StaticRTSCallbacks { + pub fn new() -> Self { + PATH_BUF.get_or_init(|| get_static_path(true)); + Self {} + } } impl Callbacks for StaticRTSCallbacks { @@ -47,8 +46,10 @@ impl Callbacks for StaticRTSCallbacks { EXCLUDED.get_or_init(|| true); } + config.opts.unstable_opts.always_encode_mir = true; + // The only possibility to intercept vtable entries, which I found, is in their local crate - config.override_queries = Some(|_session, providers, _extern_providers| { + config.override_queries = Some(|_session, providers| { // SAFETY: We store the address of the original vtable_entries function as a usize. OLD_VTABLE_ENTRIES.store(unsafe { transmute(providers.vtable_entries) }, SeqCst); @@ -77,60 +78,22 @@ impl Callbacks for StaticRTSCallbacks { } impl StaticRTSCallbacks { - pub fn new() -> Self { - PATH_BUF.get_or_init(|| get_static_path(true)); - Self { - graph: DependencyGraph::new(), - } - } - fn run_analysis(&mut self, tcx: TyCtxt) { let crate_name = format!("{}", tcx.crate_name(LOCAL_CRATE)); - let crate_id = tcx.sess.local_stable_crate_id().to_u64(); + let crate_id = tcx.stable_crate_id(LOCAL_CRATE).as_u64(); - //############################################################################################################## - // Collect all MIR bodies that are relevant for code generation - - let code_gen_units = tcx.collect_and_partition_mono_items(()).1; - - let bodies = code_gen_units - .iter() - .flat_map(|c| c.items().keys()) - .filter(|m| if let MonoItem::Fn(_) = m { true } else { false }) - .map(|m| { - let MonoItem::Fn(instance) = m else {unreachable!()}; - instance - }) - .filter(|i| tcx.is_mir_available(i.def_id())) - .filter(|i| tcx.is_codegened_item(i.def_id())) - //.filter(|i| i.def_id().is_local()) // It is not feasible to only analyze local MIR - .map(|i| (tcx.optimized_mir(i.def_id()), i.substs)) - .collect_vec(); - - //############################################################################################################## - // 1. Visit every instance (pair of MIR body and corresponding generic args) - // and every body from const - // 1) Creates the graph - // 2) Write graph to file - - let mut graph_visitor = GraphVisitor::new(tcx, &mut self.graph); - for (body, substs) in &bodies { - graph_visitor.visit(&body, substs); - } + let graph = create_dependency_graph(tcx, MonoItemCollectionMode::Lazy); + debug!("Created graph for {}", crate_name); + write_to_file( - self.graph.to_string(), + graph.to_string(), PATH_BUF.get().unwrap().clone(), |buf| get_graph_path(buf, &crate_name, crate_id), false, ); - trace!("Generated dependency graph for {}", crate_name); - - //############################################################################################################## - // Continue at shared analysis - - run_analysis_shared(tcx, bodies); + run_analysis_shared(tcx); } } @@ -153,26 +116,19 @@ fn custom_vtable_entries_monomorphized<'tcx>( if !excluded(|| tcx.crate_name(LOCAL_CRATE).as_str().to_string()) { for entry in result { if let VtblEntry::Method(instance) = entry { - let substs = if cfg!(not(feature = "monomorphize")) { - List::empty() - } else { - instance.substs - }; - - // TODO: it should be feasible to exclude closures here - - let name = def_id_name(tcx, instance.def_id(), substs, false, true).to_owned() - + SUFFIX_DYN; - - let checksum = get_checksum_vtbl_entry(tcx, &entry); - - trace!("Considering {:?} in checksums of {}", instance, name); - - insert_hashmap( - &mut *NEW_CHECKSUMS_VTBL.get().unwrap().lock().unwrap(), - &name, - checksum, - ) + let def_id = instance.def_id(); + if !tcx.is_closure(def_id) && !tcx.is_fn_trait(key.def_id()) { + let checksum = get_checksum_vtbl_entry(tcx, &entry); + let name = def_id_name(tcx, def_id, false, true).to_owned() + SUFFIX_DYN; + + trace!("Considering {:?} in checksums of {}", instance, name); + + insert_hashmap( + &mut *NEW_CHECKSUMS_VTBL.get().unwrap().lock().unwrap(), + &name, + checksum, + ) + } } } } diff --git a/src/static_rts/graph.rs b/src/static_rts/graph.rs index 7587c449..77d861c3 100644 --- a/src/static_rts/graph.rs +++ b/src/static_rts/graph.rs @@ -1,40 +1,53 @@ use itertools::Itertools; use queues::{IsQueue, Queue}; +use rustc_middle::{ + mir::mono::MonoItem, + ty::{List, TyCtxt}, +}; use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::str::FromStr; +use crate::{ + constants::SUFFIX_DYN, + names::{def_id_name, mono_def_id_name}, +}; + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] pub enum EdgeType { - Closure, - Generator, - FnDef, - FnDefTrait, - FnDefImpl, - FnDefDyn, - TraitImpl, - DynFn, + Call, + Unsize, + Contained, Drop, + Static, + ReifyPtr, + FnPtr, + + Asm, + ClosurePtr, + Intrinsic, + LangItem, Trimmed, - Monomorphization, } impl AsRef for EdgeType { fn as_ref(&self) -> &str { match self { - EdgeType::Closure => "[color = blue]", - EdgeType::Generator => "[color = green]", - EdgeType::FnDef => "[color = black]", - EdgeType::FnDefTrait => "[color = cyan]", - EdgeType::FnDefImpl => "[color = yellow]", - EdgeType::FnDefDyn => "[color = pink]", - EdgeType::TraitImpl => "[color = magenta]", - EdgeType::DynFn => "[color = magenta]", + EdgeType::Call => "[color = black]", + EdgeType::Unsize => "[color = blue]", + EdgeType::Contained => "[color = orange]", EdgeType::Drop => "[color = yellow]", + EdgeType::Static => "[color = green]", + EdgeType::ReifyPtr => "[color = magenta]", + EdgeType::FnPtr => "[color = cyan]", + + EdgeType::Asm => "[color = grey]", + EdgeType::ClosurePtr => "[color = grey]", + EdgeType::Intrinsic => "[color = grey]", + EdgeType::LangItem => "[color = grey]", EdgeType::Trimmed => "[color = red]", - EdgeType::Monomorphization => "[color = red]", } } } @@ -44,19 +57,20 @@ impl FromStr for EdgeType { fn from_str(input: &str) -> Result { match input { - "Closure" => Ok(Self::Closure), - "Generator" => Ok(Self::Generator), - "FnDef" => Ok(Self::FnDef), - "FnDefTrait" => Ok(Self::FnDefTrait), - "FnDefImpl" => Ok(Self::FnDefImpl), - "FnDefDyn" => Ok(Self::FnDefDyn), - "TraitDef" => Ok(Self::FnDef), - "TraitImpl" => Ok(Self::TraitImpl), - "DynFn" => Ok(Self::DynFn), + "Call" => Ok(Self::Call), + "Unsize" => Ok(Self::Unsize), + "Contained" => Ok(Self::Contained), "Drop" => Ok(Self::Drop), + "Static" => Ok(Self::Static), + "ReifyPtr" => Ok(Self::Static), + "FnPtr" => Ok(Self::Static), + + "ClosurePtr" => Ok(Self::ClosurePtr), + "Asm" => Ok(Self::Static), + "Intrinsic" => Ok(Self::Static), + "LangItem" => Ok(Self::Static), "Trimmed" => Ok(Self::Trimmed), - "Monomorphization" => Ok(Self::Monomorphization), _ => Err(()), } } @@ -277,6 +291,54 @@ impl DependencyGraph { } } +impl<'tcx> DependencyGraph> { + pub fn convert_to_string(self, tcx: TyCtxt<'tcx>) -> DependencyGraph { + let mut new_graph = DependencyGraph::new(); + + self.backwards_edges.into_iter().for_each(|(to, from)| { + if let Some((new_to, new_to_trimmed)) = Self::mono_item_name(tcx, to) { + new_graph.add_edge(new_to.clone(), new_to_trimmed.clone(), EdgeType::Trimmed); + + from.into_iter().for_each(|(node, types)| { + for ty in types { + if let Some((new_from, new_from_trimmed)) = Self::mono_item_name(tcx, node) + { + new_graph.add_edge( + new_from.clone(), + new_from_trimmed, + EdgeType::Trimmed, + ); + if ty == EdgeType::Unsize { + new_graph.add_edge( + new_from.clone(), + new_to_trimmed.clone() + SUFFIX_DYN, + ty, + ); + } + new_graph.add_edge(new_from, new_to.clone(), ty); + } + } + }); + } + }); + + new_graph + } + + fn mono_item_name(tcx: TyCtxt<'tcx>, mono_item: MonoItem<'tcx>) -> Option<(String, String)> { + match mono_item { + MonoItem::Fn(instance) => Some(( + mono_def_id_name(tcx, instance.def_id(), instance.args, false, true), + def_id_name(tcx, instance.def_id(), false, true), + )), + MonoItem::Static(def_id) => Some(( + mono_def_id_name(tcx, def_id, List::empty(), false, true), + def_id_name(tcx, def_id, false, true), + )), + MonoItem::GlobalAsm(_item_id) => None, + } + } +} impl ToString for DependencyGraph { fn to_string(&self) -> String { let mut result = String::new(); @@ -345,47 +407,9 @@ mod test { let mut graph: DependencyGraph = DependencyGraph::new(); graph.add_node("lonely_node".to_string()); - graph.add_edge("start1".to_string(), "end1".to_string(), EdgeType::Closure); - graph.add_edge("start1".to_string(), "end2".to_string(), EdgeType::Closure); - graph.add_edge("start2".to_string(), "end2".to_string(), EdgeType::Closure); - - let serialized = graph.to_string(); - let deserialized = DependencyGraph::from_str(&serialized).unwrap(); - - assert_eq!(graph, deserialized); - } - - #[test] - pub fn test_graph_deserialization_edge_types() { - let mut graph: DependencyGraph = DependencyGraph::new(); - - graph.add_edge( - "start1".to_string(), - "end1".to_string(), - EdgeType::TraitImpl, - ); - graph.add_edge( - "start1".to_string(), - "end1".to_string(), - EdgeType::TraitImpl, - ); - graph.add_edge("start1".to_string(), "end1".to_string(), EdgeType::Closure); - graph.add_edge( - "start1".to_string(), - "end1".to_string(), - EdgeType::Generator, - ); - graph.add_edge("start1".to_string(), "end1".to_string(), EdgeType::FnDef); - graph.add_edge( - "start1".to_string(), - "end1".to_string(), - EdgeType::FnDefTrait, - ); - graph.add_edge( - "start1".to_string(), - "end1".to_string(), - EdgeType::FnDefImpl, - ); + graph.add_edge("start1".to_string(), "end1".to_string(), EdgeType::Call); + graph.add_edge("start1".to_string(), "end2".to_string(), EdgeType::Unsize); + graph.add_edge("start2".to_string(), "end2".to_string(), EdgeType::Drop); let serialized = graph.to_string(); let deserialized = DependencyGraph::from_str(&serialized).unwrap(); diff --git a/src/static_rts/mod.rs b/src/static_rts/mod.rs new file mode 100644 index 00000000..3398b993 --- /dev/null +++ b/src/static_rts/mod.rs @@ -0,0 +1,3 @@ +pub mod callback; +pub mod graph; +pub mod visitor; diff --git a/src/static_rts/visitor.rs b/src/static_rts/visitor.rs index 75717fb4..64e27a44 100644 --- a/src/static_rts/visitor.rs +++ b/src/static_rts/visitor.rs @@ -1,569 +1,1536 @@ -use super::graph::{DependencyGraph, EdgeType}; -use crate::callbacks_shared::TEST_MARKER; -use crate::constants::SUFFIX_DYN; -use crate::names::def_id_name; -use log::warn; +// Inspired by rustc_monomorphize::collector +// Source: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_monomorphize/collector.rs.html +// +// Adapted to extract the dependency relation instead of monomorphization + +use hir::{AttributeMap, ConstContext}; +use itertools::Itertools; +use log::trace; +use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::sync::{par_for_each_in, MTLock, MTLockRef}; +use rustc_hir as hir; use rustc_hir::def::DefKind; -use rustc_hir::AttributeMap; -use rustc_middle::mir::visit::{TyContext, Visitor}; -use rustc_middle::mir::Body; -use rustc_middle::ty::{GenericArg, Instance, InstanceDef, List, Ty, TyCtxt, TyKind}; -use rustc_span::def_id::DefId; - -/// MIR Visitor responsible for creating the dependency graph and comparing checksums -pub(crate) struct GraphVisitor<'tcx, 'g> { - tcx: TyCtxt<'tcx>, - graph: &'g mut DependencyGraph, - processed_instance: Option<(DefId, &'tcx List>)>, +use rustc_hir::def_id::{DefId, DefIdMap}; +use rustc_hir::lang_items::LangItem; +use rustc_middle::mir::{self, Location}; +use rustc_middle::mir::{ + interpret::{AllocId, ErrorHandled, GlobalAlloc, Scalar}, + BasicBlock, +}; +use rustc_middle::query::TyCtxtAt; +use rustc_middle::span_bug; +use rustc_middle::ty::adjustment::{CustomCoerceUnsized, PointerCoercion}; +use rustc_middle::ty::layout::ValidityRequirement; +use rustc_middle::ty::{ + self, Instance, InstanceDef, Ty, TyCtxt, TypeFoldable, TypeVisitableExt, VtblEntry, +}; +use rustc_middle::ty::{GenericArgKind, GenericArgs}; +use rustc_middle::{bug, traits}; +use rustc_middle::{ + middle::codegen_fn_attrs::CodegenFnAttrFlags, mir::visit::TyContext, ty::GenericParamDefKind, +}; +use rustc_middle::{mir::visit::Visitor as MirVisitor, ty::List}; +use rustc_middle::{ + mir::{mono::MonoItem, Operand, TerminatorKind}, + ty::TyKind, +}; +use rustc_session::{config::EntryFnType, Limit}; +use rustc_span::{def_id::LocalDefId, symbol::sym}; +use rustc_span::{ + source_map::{dummy_spanned, respan, Spanned}, + ErrorGuaranteed, +}; +use rustc_span::{Span, DUMMY_SP}; +use std::path::PathBuf; + +use crate::{ + callbacks_shared::TEST_MARKER, + names::{def_id_name, mono_def_id_name}, + static_rts::graph::EdgeType, +}; + +use super::graph::DependencyGraph; + +// pub static debug: AtomicBool = AtomicBool::new(false); + +#[derive(PartialEq)] +pub enum MonoItemCollectionMode { + Eager, + Lazy, +} - #[cfg(not(feature = "monomorphize"))] - original_substs: Option<&'tcx List>>, +#[derive(PartialEq, Debug)] +pub enum MonomorphizationContext { + Root, + Local(EdgeType), + NonLocal(EdgeType), } -impl<'tcx, 'g> GraphVisitor<'tcx, 'g> { - pub(crate) fn new( - tcx: TyCtxt<'tcx>, - graph: &'g mut DependencyGraph, - ) -> GraphVisitor<'tcx, 'g> { - GraphVisitor { - tcx, - graph, - processed_instance: None, +#[derive(Debug)] +pub enum ContextError { + HasNoEdgeType, +} - #[cfg(not(feature = "monomorphize"))] - original_substs: None, +impl<'a> TryInto for &'a MonomorphizationContext { + type Error = ContextError; + + fn try_into(self) -> Result { + match self { + MonomorphizationContext::Local(edge_type) => Ok(*edge_type), + MonomorphizationContext::NonLocal(edge_type) => Ok(*edge_type), + MonomorphizationContext::Root => Err(ContextError::HasNoEdgeType), } } +} + +pub struct CustomUsageMap<'tcx> { + graph: DependencyGraph>, + tcx: TyCtxt<'tcx>, +} - pub fn visit(&mut self, body: &'tcx Body<'tcx>, substs: &'tcx List>) { - let def_id = body.source.def_id(); +type MonoItems<'tcx> = Vec<(Spanned>, MonomorphizationContext)>; - #[cfg(feature = "monomorphize")] - { - self.processed_instance = Some((def_id, substs)); +impl<'tcx> CustomUsageMap<'tcx> { + fn new(tcx: TyCtxt<'tcx>) -> CustomUsageMap<'tcx> { + CustomUsageMap { + graph: DependencyGraph::new(), + tcx, } - #[cfg(not(feature = "monomorphize"))] - { - self.processed_instance = Some((def_id, List::empty())); - self.original_substs = Some(substs); + } + + fn record_used<'a>( + &mut self, + user_item: MonoItem<'tcx>, + used_items: &[(Spanned>, MonomorphizationContext)], + ) where + 'tcx: 'a, + { + for (used_item, context) in used_items.into_iter() { + self.graph + .add_edge(user_item, used_item.node, context.try_into().unwrap()); } + } - //########################################################################################################## - // Visit body and contained promoted mir + pub fn finalize(self) -> DependencyGraph { + self.graph.convert_to_string(self.tcx) + } +} - self.visit_body(body); +pub fn create_dependency_graph<'tcx>( + tcx: TyCtxt<'tcx>, + mode: MonoItemCollectionMode, +) -> DependencyGraph { + let _prof_timer = tcx.prof.generic_activity("dependency_graph_creation"); + + let roots = tcx + .sess + .time("dependency_graph_creation_root_collections", || { + collect_roots(tcx, mode) + }); - for body in self.tcx.promoted_mir(def_id) { - self.visit_body(body) - } + trace!("building dependency graph, beginning at roots"); + + let mut visited = MTLock::new(FxHashSet::default()); + let mut usage_map = MTLock::new(CustomUsageMap::new(tcx)); + let recursion_limit = tcx.recursion_limit(); + + { + let visited: MTLockRef<'_, _> = &mut visited; + let usage_map: MTLockRef<'_, _> = &mut usage_map; + + tcx.sess.time("dependency_graph_creation_graph_walk", || { + par_for_each_in(roots, |root| { + let mut recursion_depths = DefIdMap::default(); + collect_items_rec( + tcx, + dummy_spanned(root), + visited, + &mut recursion_depths, + recursion_limit, + usage_map, + ); + }); + }); + } - self.processed_instance = None; + let mut graph = usage_map.into_inner().finalize(); - #[cfg(not(feature = "monomorphize"))] - { - self.original_substs = None; - } - } + let tests = tcx.sess.time("dependency_graph_root_collection", || { + collect_test_functions(tcx) + }); - fn get_outer(&self) -> (DefId, &'tcx List>) { - self.processed_instance - .expect("Cannot find currently analyzed body") + for test in tests { + let def_id = test.def_id(); + let name_trimmed = def_id_name(tcx, def_id, false, true); + let name = mono_def_id_name(tcx, def_id, List::empty(), false, false); + graph.add_edge(name, name_trimmed, EdgeType::Trimmed); } - #[cfg(not(feature = "monomorphize"))] - fn get_orig(&self) -> &'tcx List> { - self.original_substs - .expect("Cannot find currently original substs") + graph +} + +// Find all non-generic items by walking the HIR. These items serve as roots to +// start monomorphizing from. +fn collect_roots(tcx: TyCtxt<'_>, mode: MonoItemCollectionMode) -> Vec> { + trace!("collecting roots"); + let mut roots = Vec::new(); + + { + let entry_fn = tcx.entry_fn(()); + + trace!("collect_roots: entry_fn = {:?}", entry_fn); + + let mut collector = RootCollector { + tcx, + mode, + entry_fn, + output: &mut roots, + }; + + let crate_items = tcx.hir_crate_items(()); + + for id in crate_items.items() { + collector.process_item(id); + } + + for id in crate_items.impl_items() { + collector.process_impl_item(id); + } + + collector.push_extra_entry_roots(); } + + // We can only codegen items that are instantiable - items all of + // whose predicates hold. Luckily, items that aren't instantiable + // can't actually be used, so we can just skip codegenning them. + roots + .into_iter() + .filter_map( + |( + Spanned { + node: mono_item, .. + }, + _context, + )| { mono_item.is_instantiable(tcx).then_some(mono_item) }, + ) + .collect() } -impl<'tcx, 'g> Visitor<'tcx> for GraphVisitor<'tcx, 'g> { - fn visit_body(&mut self, body: &Body<'tcx>) { - let (outer, outer_substs) = self.get_outer(); - - if outer.is_local() { - let attrs = &self.tcx.hir_crate(()).owners[self - .tcx - .local_def_id_to_hir_id(outer.expect_local()) - .owner - .def_id] - .as_owner() - .map_or(AttributeMap::EMPTY, |o| &o.attrs) - .map; - - for (_, list) in attrs.iter() { - for attr in *list { - if attr.name_or_empty().to_ident_string() == TEST_MARKER { - let def_path = def_id_name(self.tcx, outer, outer_substs, false, false); - let trimmed_def_path = - def_id_name(self.tcx, outer, outer_substs, false, true); - - self.graph.add_edge( - def_path[0..def_path.len() - 13].to_string(), - trimmed_def_path[0..trimmed_def_path.len() - 13].to_string(), - EdgeType::Trimmed, - ) +// Find all test functions. These items serve as roots to start building the dependency graph from. +pub fn collect_test_functions(tcx: TyCtxt<'_>) -> Vec> { + trace!("collecting test functions"); + let mut roots = Vec::new(); + { + for def in tcx.mir_keys(()) { + let const_context = tcx.hir().body_const_context(*def); + if let Some(ConstContext::ConstFn) | None = const_context { + let attrs = &tcx.hir_crate(()).owners + [tcx.local_def_id_to_hir_id(*def).owner.def_id] + .as_owner() + .map_or(AttributeMap::EMPTY, |o| &o.attrs) + .map; + + let is_test = attrs + .iter() + .flat_map(|(_, list)| list.iter()) + .unique_by(|i| i.id) + .any(|attr| attr.name_or_empty().to_ident_string() == TEST_MARKER); + + if is_test { + let body = tcx.optimized_mir(def.to_def_id()); + let maybe_first_bb = body.basic_blocks.get(BasicBlock::from_usize(0)); + let first_call = maybe_first_bb.and_then(|bb| bb.terminator.as_ref()); + + if let Some(terminator) = first_call { + if let TerminatorKind::Call { func, .. } = &terminator.kind { + if let Operand::Constant(const_operand) = func { + let ty = const_operand.ty(); + if let TyKind::FnDef(def_id, substs) = ty.kind() { + let instance = Instance::new(*def_id, substs); + let mono_item = MonoItem::Fn(instance); + roots.push(dummy_spanned(mono_item)) + } + } + } } } } } + } - #[cfg(feature = "monomorphize")] - { - let name_after_monomorphization = - def_id_name(self.tcx, outer, outer_substs, false, true); - let name_not_monomorphized = def_id_name(self.tcx, outer, &[], false, true); - - self.graph.add_edge( - name_after_monomorphization, - name_not_monomorphized, - EdgeType::Monomorphization, - ); - } + // We can only codegen items that are instantiable - items all of + // whose predicates hold. Luckily, items that aren't instantiable + // can't actually be used, so we can just skip codegenning them. + roots + .into_iter() + .filter_map( + |Spanned { + node: mono_item, .. + }| { mono_item.is_instantiable(tcx).then_some(mono_item) }, + ) + .collect() +} - if let DefKind::AssocFn = self.tcx.def_kind(outer) { - let name = def_id_name(self.tcx, outer, &outer_substs, false, true); +/// Collect all monomorphized items reachable from `starting_point` +fn collect_items_rec<'tcx>( + tcx: TyCtxt<'tcx>, + starting_item: Spanned>, + visited: MTLockRef<'_, FxHashSet>>, + recursion_depths: &mut DefIdMap, + recursion_limit: Limit, + usage_map: MTLockRef<'_, CustomUsageMap<'tcx>>, +) { + if !visited.lock_mut().insert(starting_item.node) { + // We've been here already, no need to search again. + return; + } - // 5. (only associated functions) function + !dyn -> function - self.graph - .add_edge(name.clone() + SUFFIX_DYN, name, EdgeType::DynFn) - } + let mut used_items: MonoItems = Vec::new(); + let recursion_depth_reset; - if let Some(impl_def) = self.tcx.impl_of_method(outer) { - if let Some(_) = self.tcx.impl_trait_ref(impl_def) { - let implementors = self.tcx.impl_item_implementor_ids(impl_def); + match starting_item.node { + MonoItem::Static(def_id) => { + let instance = Instance::mono(tcx, def_id); - // 4. function in `trait` definition + !dyn -> function in trait impl (`impl for ..`) + !dyn - for (trait_fn, impl_fn) in implementors { - if *impl_fn == outer { - let name_trait_fn = - def_id_name(self.tcx, *trait_fn, &[], false, true) + SUFFIX_DYN; // No substs here, even with monomorphize - let name_impl_fn = - def_id_name(self.tcx, outer, outer_substs, false, true) + SUFFIX_DYN; + // Sanity check whether this ended up being collected accidentally + debug_assert!(should_codegen_locally(tcx, &instance)); + + let ty = instance.ty(tcx, ty::ParamEnv::reveal_all()); + visit_drop_use(tcx, ty, true, starting_item.span, &mut used_items); + + recursion_depth_reset = None; + + if let Ok(alloc) = tcx.eval_static_initializer(def_id) { + for &prov in alloc.inner().provenance().ptrs().values() { + collect_alloc(tcx, prov.alloc_id(), &mut used_items); + } + } - self.graph - .add_edge(name_trait_fn, name_impl_fn, EdgeType::TraitImpl); + if tcx.needs_thread_local_shim(def_id) { + used_items.push(( + respan( + starting_item.span, + MonoItem::Fn(Instance { + def: InstanceDef::ThreadLocalShim(def_id), + args: GenericArgs::empty(), + }), + ), + // 5.1. function -> accessed static variable + MonomorphizationContext::Local(EdgeType::Static), + )); + } + } + MonoItem::Fn(instance) => { + // Sanity check whether this ended up being collected accidentally + debug_assert!(should_codegen_locally(tcx, &instance)); + + // Keep track of the monomorphization recursion depth + recursion_depth_reset = Some(check_recursion_limit( + tcx, + instance, + starting_item.span, + recursion_depths, + recursion_limit, + )); + check_type_length_limit(tcx, instance); + + rustc_data_structures::stack::ensure_sufficient_stack(|| { + collect_used_items(tcx, instance, &mut used_items); + }); + } + MonoItem::GlobalAsm(item_id) => { + recursion_depth_reset = None; + + let item = tcx.hir().item(item_id); + if let hir::ItemKind::GlobalAsm(asm) = item.kind { + for (op, op_sp) in asm.operands { + match op { + hir::InlineAsmOperand::Const { .. } => { + // Only constants which resolve to a plain integer + // are supported. Therefore the value should not + // depend on any other items. + } + hir::InlineAsmOperand::SymFn { anon_const } => { + let fn_ty = tcx + .typeck_body(anon_const.body) + .node_type(anon_const.hir_id); + visit_fn_use(tcx, fn_ty, false, *op_sp, &mut used_items, EdgeType::Asm); + } + hir::InlineAsmOperand::SymStatic { path: _, def_id } => { + let instance = Instance::mono(tcx, *def_id); + if should_codegen_locally(tcx, &instance) { + trace!("collecting static {:?}", def_id); + used_items.push(( + dummy_spanned(MonoItem::Static(*def_id)), + MonomorphizationContext::Local(EdgeType::Static), + )); + } + } + hir::InlineAsmOperand::In { .. } + | hir::InlineAsmOperand::Out { .. } + | hir::InlineAsmOperand::InOut { .. } + | hir::InlineAsmOperand::SplitInOut { .. } => { + span_bug!(*op_sp, "invalid operand type for global_asm!") + } } } + } else { + span_bug!( + item.span, + "Mismatch between hir::Item type and MonoItem type" + ) } } + } + + usage_map + .lock_mut() + .record_used(starting_item.node, &used_items); + + for (used_item, context) in used_items { + if let MonomorphizationContext::Local(_) = context { + collect_items_rec( + tcx, + used_item, + visited, + recursion_depths, + recursion_limit, + usage_map, + ); + } + } - self.super_body(body); + if let Some((def_id, depth)) = recursion_depth_reset { + recursion_depths.insert(def_id, depth); } +} - #[allow(unused_mut)] - fn visit_ty(&mut self, mut ty: Ty<'tcx>, _ty_context: TyContext) { - self.super_ty(ty); - let (outer, mut outer_substs) = self.get_outer(); - let outer_name = def_id_name(self.tcx, outer, outer_substs, false, true); +/// Format instance name that is already known to be too long for rustc. +/// Show only the first 2 types if it is longer than 32 characters to avoid blasting +/// the user's terminal with thousands of lines of type-name. +/// +/// If the type name is longer than before+after, it will be written to a file. +fn shrunk_instance_name<'tcx>( + tcx: TyCtxt<'tcx>, + instance: &Instance<'tcx>, +) -> (String, Option) { + let s = instance.to_string(); + + // Only use the shrunk version if it's really shorter. + // This also avoids the case where before and after slices overlap. + if s.chars().nth(33).is_some() { + let shrunk = format!("{}", ty::ShortInstance(instance, 4)); + if shrunk == s { + return (s, None); + } - #[cfg(not(feature = "monomorphize"))] - let orig_substs = self.get_orig(); + let path = tcx + .output_filenames(()) + .temp_path_ext("long-type.txt", None); + let written_to_path = std::fs::write(&path, s).ok().map(|_| path); - #[cfg(feature = "monomorphize")] - let orig_substs = outer_substs; + (shrunk, written_to_path) + } else { + (s, None) + } +} - // 6. function -> destructor (`drop()` function) of referenced abstract datatype - if let Some(adt_def) = ty.ty_adt_def() { - if let Some(destructor) = self.tcx.adt_destructor(adt_def.did()) { - self.graph.add_edge( - outer_name.clone(), - def_id_name(self.tcx, destructor.did, &[], false, true), - EdgeType::Drop, - ); - } - } +fn check_recursion_limit<'tcx>( + tcx: TyCtxt<'tcx>, + instance: Instance<'tcx>, + span: Span, + recursion_depths: &mut DefIdMap, + recursion_limit: Limit, +) -> (DefId, usize) { + let def_id = instance.def_id(); + let recursion_depth = recursion_depths.get(&def_id).cloned().unwrap_or(0); + trace!(" => recursion depth={}", recursion_depth); + + let adjusted_recursion_depth = if Some(def_id) == tcx.lang_items().drop_in_place_fn() { + // HACK: drop_in_place creates tight monomorphization loops. Give + // it more margin. + recursion_depth / 4 + } else { + recursion_depth + }; + + // Code that needs to instantiate the same function recursively + // more than the recursion limit is assumed to be causing an + // infinite expansion. + if !recursion_limit.value_within_limit(adjusted_recursion_depth) { + let def_span = tcx.def_span(def_id); + let def_path_str = tcx.def_path_str(def_id); + let (shrunk, written_to_path) = shrunk_instance_name(tcx, &instance); + let mut path = PathBuf::new(); + let was_written = if let Some(written_to_path) = written_to_path { + path = written_to_path; + Some(()) + } else { + None + }; + panic!( + "Reached recursion limit {:?} {} {:?} {} {:?} {:?}", + span, shrunk, def_span, def_path_str, was_written, path, + ); + } + + recursion_depths.insert(def_id, recursion_depth + 1); + + (def_id, recursion_depth) +} - #[allow(unused_variables)] - if let Some((def_id, substs, edge_type)) = match ty.kind() { - // 1. function -> contained Closure - TyKind::Closure(def_id, substs) => Some((*def_id, *substs, EdgeType::Closure)), - // 2. function -> contained Generator - TyKind::Generator(def_id, substs, _) => Some((*def_id, *substs, EdgeType::Generator)), +fn check_type_length_limit<'tcx>(tcx: TyCtxt<'tcx>, instance: Instance<'tcx>) { + let type_length = instance + .args + .iter() + .flat_map(|arg| arg.walk()) + .filter(|arg| match arg.unpack() { + GenericArgKind::Type(_) | GenericArgKind::Const(_) => true, + GenericArgKind::Lifetime(_) => false, + }) + .count(); + trace!(" => type length={}", type_length); + + // Rust code can easily create exponentially-long types using only a + // polynomial recursion depth. Even with the default recursion + // depth, you can easily get cases that take >2^60 steps to run, + // which means that rustc basically hangs. + // + // Bail out in these cases to avoid that bad user experience. + if !tcx.type_length_limit().value_within_limit(type_length) { + let (shrunk, written_to_path) = shrunk_instance_name(tcx, &instance); + let span = tcx.def_span(instance.def_id()); + let mut path = PathBuf::new(); + let was_written = if let Some(path2) = written_to_path { + path = path2; + Some(()) + } else { + None + }; + panic!( + "Reached type length limit {:?} {} {:?} {:?} {}", + span, shrunk, was_written, path, type_length, + ); + } +} - TyKind::FnDef(_def_id, _substs) => { - // We need to resolve ty here, to precisely resolve statically dispatched function calls - let param_env = self - .tcx - .param_env(outer) - .with_reveal_all_normalized(self.tcx); - ty = self - .tcx - .subst_and_normalize_erasing_regions(orig_substs, param_env, ty); +struct MirUsedCollector<'a, 'tcx> { + tcx: TyCtxt<'tcx>, + body: &'a mir::Body<'tcx>, + output: &'a mut MonoItems<'tcx>, + instance: Instance<'tcx>, + visiting_call_terminator: bool, +} - let TyKind::FnDef(mut def_id, mut substs) = ty.kind() else {unreachable!()}; +impl<'a, 'tcx> MirUsedCollector<'a, 'tcx> { + pub fn monomorphize(&self, value: T) -> T + where + T: TypeFoldable>, + { + trace!("monomorphize: self.instance={:?}", self.instance); + self.instance.instantiate_mir_and_normalize_erasing_regions( + self.tcx, + ty::ParamEnv::reveal_all(), + ty::EarlyBinder::bind(value), + ) + } +} - let maybe_resolved = if let Ok(Some(instance)) = - Instance::resolve(self.tcx, param_env, def_id, &substs) +impl<'a, 'tcx> MirVisitor<'tcx> for MirUsedCollector<'a, 'tcx> { + // fn visit_body(&mut self, body: &mir::Body<'tcx>) { + // let name = def_id_name(self.tcx, body.source.def_id(), false, true); + // if name.contains("spans_field_collision") { + // info!("Found body {:?}", body); + // } + // self.super_body(body); + // } + + fn visit_rvalue(&mut self, rvalue: &mir::Rvalue<'tcx>, location: Location) { + trace!("visiting rvalue {:?}", *rvalue); + + let span = self.body.source_info(location).span; + + match *rvalue { + // When doing an cast from a regular pointer to a fat pointer, we + // have to instantiate all methods of the trait being cast to, so we + // can build the appropriate vtable. + mir::Rvalue::Cast( + mir::CastKind::PointerCoercion(PointerCoercion::Unsize), + ref operand, + target_ty, + ) + | mir::Rvalue::Cast(mir::CastKind::DynStar, ref operand, target_ty) => { + let target_ty = self.monomorphize(target_ty); + let source_ty = operand.ty(self.body, self.tcx); + let source_ty = self.monomorphize(source_ty); + let (source_ty, target_ty) = + find_vtable_types_for_unsizing(self.tcx.at(span), source_ty, target_ty); + // This could also be a different Unsize instruction, like + // from a fixed sized array to a slice. But we are only + // interested in things that produce a vtable. + if (target_ty.is_trait() && !source_ty.is_trait()) + || (target_ty.is_dyn_star() && !source_ty.is_dyn_star()) { - match instance.def { - InstanceDef::Virtual(def_id, _) => { - // 3.4 caller function -> callee `fn` + !dyn (for functions in `trait {..} called by dynamic dispatch) - Some(Some((def_id, List::empty(), EdgeType::FnDefDyn))) - // No substs here, even with monomorphize - } - InstanceDef::Item(item) if !self.tcx.is_closure(instance.def_id()) => { - // Assign resolved function - def_id = item.did; - substs = instance.substs; - None - } - _ => Some(None), - } - } else { - warn!( - "Failed to resolve instance, may lead to unsafe behavior {:?} - {:?}", - def_id, substs + create_mono_items_for_vtable_methods( + self.tcx, + target_ty, + source_ty, + span, + self.output, ); - None - }; - - maybe_resolved.unwrap_or_else(|| { - if let DefKind::AssocFn = self.tcx.def_kind(def_id) { - if let Some(_trait_def_id) = self.tcx.trait_of_item(def_id) { - // 3.1 caller function -> callee `fn` (for functions in `trait {..}) - Some((def_id, substs, EdgeType::FnDefTrait)) - } else { - // 3.2 caller function -> callee `fn` (for assoc `fn`s in `impl .. {..}) - Some((def_id, substs, EdgeType::FnDefImpl)) + } + } + mir::Rvalue::Cast( + mir::CastKind::PointerCoercion(PointerCoercion::ReifyFnPointer), + ref operand, + _, + ) => { + let fn_ty = operand.ty(self.body, self.tcx); + let fn_ty = self.monomorphize(fn_ty); + visit_fn_use( + self.tcx, + fn_ty, + false, + span, + self.output, + // 6.1. function -> function that is coerced to a function pointer + EdgeType::ReifyPtr, + ); + } + mir::Rvalue::Cast( + mir::CastKind::PointerCoercion(PointerCoercion::ClosureFnPointer(_)), + ref operand, + _, + ) => { + let source_ty = operand.ty(self.body, self.tcx); + let source_ty = self.monomorphize(source_ty); + match *source_ty.kind() { + ty::Closure(def_id, args) => { + let instance = Instance::new(def_id, args); + if should_codegen_locally(self.tcx, &instance) { + self.output.push(( + create_fn_mono_item(self.tcx, instance, span), + // 6.2. function -> closure that is coerced to a function pointer + MonomorphizationContext::Local(EdgeType::ClosurePtr), + )); } - } else { - // 3.3 caller function -> callee `fn` (for non-assoc `fn`s, i.e. not inside `impl .. {..}`) - Some((def_id, substs, EdgeType::FnDef)) } - }) - } - _ => None, - } { - #[cfg(feature = "monomorphize")] - { - let mut name = def_id_name(self.tcx, def_id, substs, false, true); - if edge_type == EdgeType::FnDefDyn { - name += SUFFIX_DYN; + _ => bug!(), } - - self.graph.add_edge(outer_name.clone(), name, edge_type); } - - #[cfg(not(feature = "monomorphize"))] - { - let mut name = def_id_name(self.tcx, def_id, &[], false, true); - if edge_type == EdgeType::FnDefDyn { - name += SUFFIX_DYN; + mir::Rvalue::ThreadLocalRef(def_id) => { + assert!(self.tcx.is_thread_local_static(def_id)); + let instance = Instance::mono(self.tcx, def_id); + if should_codegen_locally(self.tcx, &instance) { + trace!("collecting thread-local static {:?}", def_id); + self.output.push(( + respan(span, MonoItem::Static(def_id)), + // 5.1. function -> accessed static variable + MonomorphizationContext::Local(EdgeType::Static), + )); } - - self.graph.add_edge(outer_name.clone(), name, edge_type); } + _ => { /* not interesting */ } } + + self.super_rvalue(rvalue, location); + } + + /// This does not walk the constant, as it has been handled entirely here and trying + /// to walk it would attempt to evaluate the `ty::Const` inside, which doesn't necessarily + /// work, as some constants cannot be represented in the type system. + fn visit_constant(&mut self, constant: &mir::ConstOperand<'tcx>, location: Location) { + let const_ = self.monomorphize(constant.const_); + let param_env = ty::ParamEnv::reveal_all(); + let val = match const_.eval(self.tcx, param_env, None) { + Ok(v) => v, + Err(ErrorHandled::Reported(..)) => return, + Err(ErrorHandled::TooGeneric(..)) => span_bug!( + self.body.source_info(location).span, + "collection encountered polymorphic constant: {:?}", + const_ + ), + }; + collect_const_value(self.tcx, val, self.output); + MirVisitor::visit_ty(self, const_.ty(), TyContext::Location(location)); } -} -#[cfg(test)] -mod test { - use itertools::Itertools; - use log::info; - use rustc_middle::mir::mono::MonoItem; - use std::{fs, io::Error, path::PathBuf, string::String}; - use test_log::test; - - use rustc_errors::registry; - use rustc_hash::{FxHashMap, FxHashSet}; - use rustc_session::config::{self, CheckCfg, OptLevel}; - use rustc_span::source_map; - - use crate::constants::SUFFIX_DYN; - use crate::static_rts::graph::{DependencyGraph, EdgeType}; - - use super::GraphVisitor; - - const TEST_DATA_PATH: &str = "test-data/static/src"; - - fn load_test_code(file_name: &str) -> Result { - let mut path_buf = PathBuf::from(TEST_DATA_PATH); - path_buf.push(file_name); - fs::read_to_string(path_buf.as_path()) - } - - fn compile_and_visit(file_name: &str) -> DependencyGraph { - let test_code = load_test_code(file_name).expect("Failed to load test code."); - - let config = rustc_interface::Config { - opts: config::Options { - test: true, - optimize: OptLevel::No, - ..config::Options::default() - }, - crate_cfg: FxHashSet::default(), - crate_check_cfg: CheckCfg::default(), - input: config::Input::Str { - name: source_map::FileName::Custom("main.rs".into()), - input: test_code, - }, - output_dir: None, - output_file: None, - file_loader: None, - lint_caps: FxHashMap::default(), - parse_sess_created: None, - register_lints: None, - override_queries: None, - registry: registry::Registry::new(&rustc_error_codes::DIAGNOSTICS), - make_codegen_backend: None, + fn visit_terminator(&mut self, terminator: &mir::Terminator<'tcx>, location: Location) { + trace!("visiting terminator {:?} @ {:?}", terminator, location); + let source = self.body.source_info(location).span; + + let tcx = self.tcx; + let push_mono_lang_item = |this: &mut Self, lang_item: LangItem| { + let instance = Instance::mono(tcx, tcx.require_lang_item(lang_item, Some(source))); + if should_codegen_locally(tcx, &instance) { + this.output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::Local(EdgeType::LangItem), + )); + } }; - let mut graph = DependencyGraph::new(); - - rustc_interface::run_compiler(config, |compiler| { - compiler.enter(|queries| { - queries.global_ctxt().unwrap().enter(|tcx| { - let code_gen_units = tcx.collect_and_partition_mono_items(()).1; - let bodies = code_gen_units - .iter() - .flat_map(|c| c.items().keys()) - .filter(|m| if let MonoItem::Fn(_) = m { true } else { false }) - .map(|m| { - let MonoItem::Fn(instance) = m else {unreachable!()}; - instance - }) - .filter(|i: &&rustc_middle::ty::Instance| tcx.is_mir_available(i.def_id())) - .map(|i| (tcx.optimized_mir(i.def_id()), i.substs)) - .collect_vec(); - - let mut visitor = GraphVisitor::new(tcx, &mut graph); - - for (body, substs) in bodies { - visitor.visit(body, substs); + match terminator.kind { + mir::TerminatorKind::Call { ref func, .. } => { + let callee_ty = func.ty(self.body, tcx); + let callee_ty = self.monomorphize(callee_ty); + visit_fn_use( + self.tcx, + callee_ty, + true, + source, + &mut self.output, + // 1. function -> callee function (static dispatch) + EdgeType::Call, + ) + } + mir::TerminatorKind::Drop { ref place, .. } => { + let ty = place.ty(self.body, self.tcx).ty; + let ty = self.monomorphize(ty); + visit_drop_use(self.tcx, ty, true, source, self.output); + } + mir::TerminatorKind::InlineAsm { ref operands, .. } => { + for op in operands { + match *op { + mir::InlineAsmOperand::SymFn { ref value } => { + let fn_ty = self.monomorphize(value.const_.ty()); + visit_fn_use( + self.tcx, + fn_ty, + false, + source, + self.output, + EdgeType::Asm, + ); + } + mir::InlineAsmOperand::SymStatic { def_id } => { + let instance = Instance::mono(self.tcx, def_id); + if should_codegen_locally(self.tcx, &instance) { + trace!("collecting asm sym static {:?}", def_id); + self.output.push(( + respan(source, MonoItem::Static(def_id)), + MonomorphizationContext::Local(EdgeType::Static), + )); + } + } + _ => {} } - }) - }); - }); + } + } + mir::TerminatorKind::Assert { ref msg, .. } => { + let lang_item = match &**msg { + mir::AssertKind::BoundsCheck { .. } => LangItem::PanicBoundsCheck, + mir::AssertKind::MisalignedPointerDereference { .. } => { + LangItem::PanicMisalignedPointerDereference + } + _ => LangItem::Panic, + }; + push_mono_lang_item(self, lang_item); + } + mir::TerminatorKind::UnwindTerminate(reason) => { + push_mono_lang_item(self, reason.lang_item()); + } + mir::TerminatorKind::Goto { .. } + | mir::TerminatorKind::SwitchInt { .. } + | mir::TerminatorKind::UnwindResume + | mir::TerminatorKind::Return + | mir::TerminatorKind::Unreachable => {} + mir::TerminatorKind::CoroutineDrop + | mir::TerminatorKind::Yield { .. } + | mir::TerminatorKind::FalseEdge { .. } + | mir::TerminatorKind::FalseUnwind { .. } => bug!(), + } + + if let Some(mir::UnwindAction::Terminate(reason)) = terminator.unwind() { + push_mono_lang_item(self, reason.lang_item()); + } - info!("{}", graph.to_string()); - graph + self.visiting_call_terminator = matches!(terminator.kind, mir::TerminatorKind::Call { .. }); + self.super_terminator(terminator, location); + self.visiting_call_terminator = false; } - fn assert_contains_edge( - graph: &DependencyGraph, - start: &str, - end: &str, - edge_type: &EdgeType, - ) { - let error_str = format!("Did not find edge {} -> {} ({:?})", start, end, edge_type); + fn visit_ty(&mut self, ty: Ty<'tcx>, context: TyContext) { + let source = self.body.span; - let start = graph.get_nodes().iter().find(|s| **s == start).unwrap(); + let tcx = self.tcx; - let end = graph.get_nodes().iter().find(|s| **s == end).unwrap(); + if let ty::TyKind::Closure(..) | TyKind::Coroutine(..) = ty.kind() { + let ty = self.monomorphize(ty); + // 2. function -> contained closure + visit_fn_use(tcx, ty, false, source, self.output, EdgeType::Contained); + } + if let TyKind::Ref(_region, ty, _mtblt) = *ty.kind() { + // Also account for closures behind references + self.visit_ty(ty, context); + } + // self.super_ty(ty) //Weirdly, this is just empty + } - let maybe_edges = graph.get_edges_to(end); - assert!(maybe_edges.is_some(), "{}", error_str); + fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, location: Location) { + self.super_operand(operand, location); + } +} - let edges = maybe_edges.unwrap(); - assert!(edges.contains_key(start), "{}", error_str); +fn visit_drop_use<'tcx>( + tcx: TyCtxt<'tcx>, + ty: Ty<'tcx>, + is_direct_call: bool, + source: Span, + output: &mut MonoItems<'tcx>, +) { + let instance = Instance::resolve_drop_in_place(tcx, ty); + visit_instance_use( + tcx, + instance, + is_direct_call, + source, + output, + // 4. function -> destructor (`drop()` function) of types that are dropped (manually or automatically) + EdgeType::Drop, + ); +} - let edge_types = edges.get(start).unwrap(); - assert!(edge_types.contains(edge_type), "{}", error_str); +fn visit_fn_use<'tcx>( + tcx: TyCtxt<'tcx>, + ty: Ty<'tcx>, + is_direct_call: bool, + source: Span, + output: &mut MonoItems<'tcx>, + edge_type: EdgeType, +) { + if let ty::FnDef(def_id, args) = *ty.kind() { + let instance = if is_direct_call { + ty::Instance::expect_resolve(tcx, ty::ParamEnv::reveal_all(), def_id, args) + } else { + match ty::Instance::resolve_for_fn_ptr(tcx, ty::ParamEnv::reveal_all(), def_id, args) { + Some(instance) => instance, + _ => bug!("failed to resolve instance for {ty}"), + } + }; + visit_instance_use(tcx, instance, is_direct_call, source, output, edge_type); } + if let ty::Coroutine(def_id, args, _) | ty::Closure(def_id, args) = *ty.kind() { + let instance = Instance::new(def_id, args); + visit_instance_use(tcx, instance, false, source, output, edge_type); + } +} - fn assert_does_not_contain_edge( - graph: &DependencyGraph, - start: &str, - end: &str, - edge_type: &EdgeType, - ) { - let start = graph - .get_nodes() - .iter() - .find(|s| s.ends_with(start)) - .unwrap(); - - let end = graph.get_nodes().iter().find(|s| s.ends_with(end)).unwrap(); - - let maybe_edges = graph.get_edges_to(end); - if maybe_edges.is_some() { - let edges = maybe_edges.unwrap(); - if edges.contains_key(start) { - let edge_types = edges.get(start).unwrap(); - assert!( - !edge_types.contains(edge_type), - "Found unexpected edge {} -> {} ({:?})", - start, - end, - edge_type - ); +fn visit_instance_use<'tcx>( + tcx: TyCtxt<'tcx>, + instance: ty::Instance<'tcx>, + is_direct_call: bool, + source: Span, + output: &mut MonoItems<'tcx>, + edge_type: EdgeType, +) { + trace!( + "visit_item_use({:?}, is_direct_call={:?})", + instance, + is_direct_call + ); + + if let ty::InstanceDef::DropGlue(_, maybe_ty) = instance.def { + if let Some(_) = maybe_ty { + // IMPORTANT: We do not want to have an indirection via drop_in_place + // and instead directly collect all drop functions that are invoked + let body = tcx.instance_mir(instance.def); + let terminators = body + .basic_blocks + .iter() + .filter_map(|bb| bb.terminator.as_ref()); + for terminator in terminators { + match terminator.kind { + mir::TerminatorKind::Call { ref func, .. } => { + let callee_ty = func.ty(body, tcx); + let callee_ty = instance.instantiate_mir_and_normalize_erasing_regions( + tcx, + ty::ParamEnv::reveal_all(), + ty::EarlyBinder::bind(callee_ty), + ); + + visit_fn_use(tcx, callee_ty, true, source, output, edge_type); + visit_drop_use(tcx, callee_ty, true, source, output); + } + _ => {} + } } } + return; + } + + // IMPORTANT: This connects the graphs of multiple crates + if tcx.is_reachable_non_generic(instance.def_id()) { + output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::NonLocal(edge_type), + )); + } + if instance + .polymorphize(tcx) + .upstream_monomorphization(tcx) + .is_some() + { + output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::Local(edge_type), // Local is necessary here + )); } - #[test] - fn test_function_call() { - let graph = compile_and_visit("call.rs"); + if let DefKind::Static(_) = tcx.def_kind(instance.def_id()) { + output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::NonLocal(edge_type), + )); + } + + if !should_codegen_locally(tcx, &instance) { + return; + } + if let ty::InstanceDef::Intrinsic(def_id) = instance.def { + let name = tcx.item_name(def_id); + if let Some(_requirement) = ValidityRequirement::from_intrinsic(name) { + // The intrinsics assert_inhabited, assert_zero_valid, and assert_mem_uninitialized_valid will + // be lowered in codegen to nothing or a call to panic_nounwind. So if we encounter any + // of those intrinsics, we need to include a mono item for panic_nounwind, else we may try to + // codegen a call to that function without generating code for the function itself. + let def_id = tcx.lang_items().get(LangItem::PanicNounwind).unwrap(); + let panic_instance = Instance::mono(tcx, def_id); + if should_codegen_locally(tcx, &panic_instance) { + output.push(( + create_fn_mono_item(tcx, panic_instance, source), + MonomorphizationContext::Local(EdgeType::Intrinsic), + )); + } + } else if tcx.has_attr(def_id, sym::rustc_safe_intrinsic) { + // Codegen the fallback body of intrinsics with fallback bodies + let instance = ty::Instance::new(def_id, instance.args); + if should_codegen_locally(tcx, &instance) { + output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::Local(EdgeType::Intrinsic), + )); + } + } + } - let start = "rust_out::test"; - let end = "rust_out::func"; - let edge_type = EdgeType::FnDef; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + match instance.def { + ty::InstanceDef::Virtual(..) | ty::InstanceDef::Intrinsic(_) => { + if !is_direct_call { + bug!("{:?} being reified", instance); + } + } + ty::InstanceDef::ThreadLocalShim(..) => { + bug!("{:?} being reified", instance); + } + ty::InstanceDef::DropGlue(..) => { + bug!("unreachable") + } + ty::InstanceDef::Item(def_id) + if tcx.is_closure(def_id) + && (edge_type != EdgeType::FnPtr && edge_type != EdgeType::Contained) => {} + ty::InstanceDef::VTableShim(..) + | ty::InstanceDef::ReifyShim(..) + | ty::InstanceDef::ClosureOnceShim { .. } + | ty::InstanceDef::Item(..) + | ty::InstanceDef::FnPtrShim(..) + | ty::InstanceDef::CloneShim(..) + | ty::InstanceDef::FnPtrAddrShim(..) => { + output.push(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::Local(edge_type), + )); + } } +} + +/// Returns `true` if we should codegen an instance in the local crate, or returns `false` if we +/// can just link to the upstream crate and therefore don't need a mono item. +fn should_codegen_locally<'tcx>(tcx: TyCtxt<'tcx>, instance: &Instance<'tcx>) -> bool { + let Some(def_id) = instance.def.def_id_if_not_guaranteed_local_codegen() else { + return true; + }; - #[test] - fn test_closure() { - let graph = compile_and_visit("closure.rs"); + if tcx.is_foreign_item(def_id) { + // Foreign items are always linked against, there's no way of instantiating them. + return false; + } - let start = "rust_out::test"; - let end = "rust_out::test::{closure#0}"; - let edge_type = EdgeType::Closure; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + if def_id.is_local() { + // Local items cannot be referred to locally without monomorphizing them locally. + return true; } - #[test] - fn test_fndef() { - let graph = compile_and_visit("fndef.rs"); + if tcx.is_reachable_non_generic(def_id) + || instance + .polymorphize(tcx) + .upstream_monomorphization(tcx) + .is_some() + { + return false; + } - let start = "rust_out::test_indirect"; - let end = "rust_out::incr"; - let edge_type = EdgeType::FnDef; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + if let DefKind::Static(_) = tcx.def_kind(def_id) { + // We cannot monomorphize statics from upstream crates. + return false; + } - let start = "rust_out::test_higher_order"; - let end = "rust_out::incr"; - let edge_type = EdgeType::FnDef; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + if !tcx.is_mir_available(def_id) { + panic!( + "Unable to find optimized MIR {:?} {}", + tcx.def_span(def_id), + tcx.crate_name(def_id.krate), + ); } - #[test] - #[cfg(not(feature = "monomorphize"))] - fn test_impls() { - let graph = compile_and_visit("impls.rs"); + true +} + +/// For a given pair of source and target type that occur in an unsizing coercion, +/// this function finds the pair of types that determines the vtable linking +/// them. +/// +/// For example, the source type might be `&SomeStruct` and the target type +/// might be `&dyn SomeTrait` in a cast like: +/// +/// ```rust,ignore (not real code) +/// let src: &SomeStruct = ...; +/// let target = src as &dyn SomeTrait; +/// ``` +/// +/// Then the output of this function would be (SomeStruct, SomeTrait) since for +/// constructing the `target` fat-pointer we need the vtable for that pair. +/// +/// Things can get more complicated though because there's also the case where +/// the unsized type occurs as a field: +/// +/// ```rust +/// struct ComplexStruct { +/// a: u32, +/// b: f64, +/// c: T +/// } +/// ``` +/// +/// In this case, if `T` is sized, `&ComplexStruct` is a thin pointer. If `T` +/// is unsized, `&SomeStruct` is a fat pointer, and the vtable it points to is +/// for the pair of `T` (which is a trait) and the concrete type that `T` was +/// originally coerced from: +/// +/// ```rust,ignore (not real code) +/// let src: &ComplexStruct = ...; +/// let target = src as &ComplexStruct; +/// ``` +/// +/// Again, we want this `find_vtable_types_for_unsizing()` to provide the pair +/// `(SomeStruct, SomeTrait)`. +/// +/// Finally, there is also the case of custom unsizing coercions, e.g., for +/// smart pointers such as `Rc` and `Arc`. +fn find_vtable_types_for_unsizing<'tcx>( + tcx: TyCtxtAt<'tcx>, + source_ty: Ty<'tcx>, + target_ty: Ty<'tcx>, +) -> (Ty<'tcx>, Ty<'tcx>) { + let ptr_vtable = |inner_source: Ty<'tcx>, inner_target: Ty<'tcx>| { + let param_env = ty::ParamEnv::reveal_all(); + let type_has_metadata = |ty: Ty<'tcx>| -> bool { + if ty.is_sized(tcx.tcx, param_env) { + return false; + } + let tail = tcx.struct_tail_erasing_lifetimes(ty, param_env); + match tail.kind() { + ty::Foreign(..) => false, + ty::Str | ty::Slice(..) | ty::Dynamic(..) => true, + _ => bug!("unexpected unsized tail: {:?}", tail), + } + }; + if type_has_metadata(inner_source) { + (inner_source, inner_target) + } else { + tcx.struct_lockstep_tails_erasing_lifetimes(inner_source, inner_target, param_env) + } + }; + + match (&source_ty.kind(), &target_ty.kind()) { + (&ty::Ref(_, a, _), &ty::Ref(_, b, _) | &ty::RawPtr(ty::TypeAndMut { ty: b, .. })) + | (&ty::RawPtr(ty::TypeAndMut { ty: a, .. }), &ty::RawPtr(ty::TypeAndMut { ty: b, .. })) => { + ptr_vtable(*a, *b) + } + (&ty::Adt(def_a, _), &ty::Adt(def_b, _)) if def_a.is_box() && def_b.is_box() => { + ptr_vtable(source_ty.boxed_ty(), target_ty.boxed_ty()) + } + + // T as dyn* Trait + (_, &ty::Dynamic(_, _, ty::DynStar)) => ptr_vtable(source_ty, target_ty), + + (&ty::Adt(source_adt_def, source_args), &ty::Adt(target_adt_def, target_args)) => { + assert_eq!(source_adt_def, target_adt_def); - let edge_type = EdgeType::FnDefImpl; - let end: &str = "rust_out::Foo::new"; + let CustomCoerceUnsized::Struct(coerce_index) = + match custom_coerce_unsize_info(tcx, source_ty, target_ty) { + Ok(ccu) => ccu, + Err(e) => { + let e = Ty::new_error(tcx.tcx, e); + return (e, e); + } + }; - let start = "rust_out::test_static"; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + let source_fields = &source_adt_def.non_enum_variant().fields; + let target_fields = &target_adt_def.non_enum_variant().fields; - let start = "rust_out::test_const"; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + assert!( + coerce_index.index() < source_fields.len() + && source_fields.len() == target_fields.len() + ); - let start = "rust_out::test_mut"; - assert_contains_edge(&graph, &start, &end, &edge_type); - assert_does_not_contain_edge(&graph, &end, &start, &edge_type); + find_vtable_types_for_unsizing( + tcx, + source_fields[coerce_index].ty(*tcx, source_args), + target_fields[coerce_index].ty(*tcx, target_args), + ) + } + _ => bug!( + "find_vtable_types_for_unsizing: invalid coercion {:?} -> {:?}", + source_ty, + target_ty + ), } +} - #[test] - #[cfg(not(feature = "monomorphize"))] - fn test_traits() { - let graph = compile_and_visit("traits.rs"); +fn create_fn_mono_item<'tcx>( + tcx: TyCtxt<'tcx>, + instance: Instance<'tcx>, + source: Span, +) -> Spanned> { + respan(source, MonoItem::Fn(instance.polymorphize(tcx))) +} - println!("{}", graph.to_string()); +/// Creates a `MonoItem` for each method that is referenced by the vtable for +/// the given trait/impl pair. +fn create_mono_items_for_vtable_methods<'tcx>( + tcx: TyCtxt<'tcx>, + trait_ty: Ty<'tcx>, + impl_ty: Ty<'tcx>, + source: Span, + output: &mut MonoItems<'tcx>, +) { + assert!(!trait_ty.has_escaping_bound_vars() && !impl_ty.has_escaping_bound_vars()); + + if let ty::Dynamic(trait_ty, ..) = trait_ty.kind() { + if let Some(principal) = trait_ty.principal() { + let poly_trait_ref = principal.with_self_ty(tcx, impl_ty); + assert!(!poly_trait_ref.has_escaping_bound_vars()); + + // Walk all methods of the trait, including those of its supertraits + let entries = tcx.vtable_entries(poly_trait_ref); + let methods = entries + .iter() + .filter_map(|entry| match entry { + VtblEntry::MetadataDropInPlace + | VtblEntry::MetadataSize + | VtblEntry::MetadataAlign + | VtblEntry::Vacant => None, + VtblEntry::TraitVPtr(_) => { + // all super trait items already covered, so skip them. + None + } + VtblEntry::Method(instance) => { + Some(*instance) // .filter(|instance| should_codegen_locally(tcx, instance)) // Fitering later on + } + }) + .filter_map(|item| { + let instance = item; + + // IMPORTANT: This connects the graphs of multiple crates + if tcx.is_reachable_non_generic(instance.def_id()) { + return Some(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::NonLocal(EdgeType::Unsize), + )); + } + if instance + .polymorphize(tcx) + .upstream_monomorphization(tcx) + .is_some() + { + return Some(( + create_fn_mono_item(tcx, instance, source), + MonomorphizationContext::Local(EdgeType::Unsize), // Local is necessary here + )); + } - { - let start = "rust_out::test_direct"; - let end = "rust_out::::sound"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); + if !should_codegen_locally(tcx, &instance) { + return None; + } - let start = "rust_out::sound_generic"; - let end = "rust_out::::sound"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); + Some(( + create_fn_mono_item(tcx, item, source), + // 2.1 function -> function in the vtable of a type that is converted into a dynamic trait object (unsized coercion) + // 2.1 function -> function in the vtable of a type that is converted into a dynamic trait object (unsized coercion) + !dyn + MonomorphizationContext::Local(EdgeType::Unsize), + )) + }); + output.extend(methods); + } - let start = "rust_out::sound_dyn"; - let end = "rust_out::Animal::sound".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefDyn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefDyn); + // Also add the destructor. + visit_drop_use(tcx, impl_ty, false, source, output); + } +} - let start = "rust_out::Animal::sound".to_owned() + SUFFIX_DYN; - let end = "rust_out::::sound".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::TraitImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::TraitImpl); +/// Scans the MIR in order to find function calls, closures, and drop-glue. +fn collect_used_items<'tcx>( + tcx: TyCtxt<'tcx>, + instance: Instance<'tcx>, + output: &mut MonoItems<'tcx>, +) { + let body = tcx.instance_mir(instance.def); + + // Here we rely on the visitor also visiting `required_consts`, so that we evaluate them + // and abort compilation if any of them errors. + MirUsedCollector { + tcx, + body, + output, + instance, + visiting_call_terminator: false, + } + .visit_body(body); +} - let start = "rust_out::Animal::walk".to_owned() + SUFFIX_DYN; - let end = "rust_out::Animal::walk"; - assert_contains_edge(&graph, &start, &end, &EdgeType::DynFn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::DynFn); +fn collect_const_value<'tcx>( + tcx: TyCtxt<'tcx>, + value: mir::ConstValue<'tcx>, + output: &mut MonoItems<'tcx>, +) { + match value { + mir::ConstValue::Scalar(Scalar::Ptr(ptr, _size)) => { + collect_alloc(tcx, ptr.provenance.alloc_id(), output) + } + mir::ConstValue::Indirect { alloc_id, .. } => collect_alloc(tcx, alloc_id, output), + mir::ConstValue::Slice { data, meta: _ } => { + for &prov in data.inner().provenance().ptrs().values() { + collect_alloc(tcx, prov.alloc_id(), output); + } } + _ => {} + } +} - { - let start = "rust_out::test_mut_direct"; - let end = "rust_out::::set_treat"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); +//=----------------------------------------------------------------------------- +// Root Collection +//=----------------------------------------------------------------------------- - let start = "rust_out::set_treat_generic"; - let end = "rust_out::::set_treat"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); +struct RootCollector<'a, 'tcx> { + tcx: TyCtxt<'tcx>, + mode: MonoItemCollectionMode, + output: &'a mut MonoItems<'tcx>, + entry_fn: Option<(DefId, EntryFnType)>, +} - let start = "rust_out::set_treat_dyn"; - let end = "rust_out::Animal::set_treat".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefDyn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefDyn); +impl<'v> RootCollector<'_, 'v> { + fn process_item(&mut self, id: hir::ItemId) { + match self.tcx.def_kind(id.owner_id) { + DefKind::Enum | DefKind::Struct | DefKind::Union => { + if self.mode == MonoItemCollectionMode::Eager + && self.tcx.generics_of(id.owner_id).count() == 0 + { + trace!("RootCollector: ADT drop-glue for `{id:?}`",); + + let ty = self + .tcx + .type_of(id.owner_id.to_def_id()) + .no_bound_vars() + .unwrap(); + visit_drop_use(self.tcx, ty, true, DUMMY_SP, self.output); + } + } + DefKind::GlobalAsm => { + trace!( + "RootCollector: ItemKind::GlobalAsm({})", + self.tcx.def_path_str(id.owner_id) + ); + self.output.push(( + dummy_spanned(MonoItem::GlobalAsm(id)), + MonomorphizationContext::Root, + )); + } + DefKind::Static(..) => { + let def_id = id.owner_id.to_def_id(); + trace!( + "RootCollector: ItemKind::Static({})", + self.tcx.def_path_str(def_id) + ); + self.output.push(( + dummy_spanned(MonoItem::Static(def_id)), + MonomorphizationContext::Root, + )); + } + DefKind::Const => { + // const items only generate mono items if they are + // actually used somewhere. Just declaring them is insufficient. - let start = "rust_out::Animal::set_treat".to_owned() + SUFFIX_DYN; - let end = "rust_out::::set_treat".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::TraitImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::TraitImpl); + // but even just declaring them must collect the items they refer to + if let Ok(val) = self.tcx.const_eval_poly(id.owner_id.to_def_id()) { + collect_const_value(self.tcx, val, self.output); + } + } + DefKind::Impl { .. } => { + if self.mode == MonoItemCollectionMode::Eager { + create_mono_items_for_default_impls(self.tcx, id, self.output); + } + } + DefKind::Fn => { + self.push_if_root(id.owner_id.def_id); + } + _ => {} } } - #[test] - #[cfg(feature = "monomorphize")] - fn test_traits() { - let graph = compile_and_visit("traits.rs"); + fn process_impl_item(&mut self, id: hir::ImplItemId) { + if matches!(self.tcx.def_kind(id.owner_id), DefKind::AssocFn) { + self.push_if_root(id.owner_id.def_id); + } + } - println!("{}", graph.to_string()); + fn is_root(&self, def_id: LocalDefId) -> bool { + !self + .tcx + .generics_of(def_id) + .requires_monomorphization(self.tcx) + && match self.mode { + MonoItemCollectionMode::Eager => true, + MonoItemCollectionMode::Lazy => { + self.entry_fn.and_then(|(id, _)| id.as_local()) == Some(def_id) + || self.tcx.is_reachable_non_generic(def_id) + || self + .tcx + .codegen_fn_attrs(def_id) + .flags + .contains(CodegenFnAttrFlags::RUSTC_STD_INTERNAL_SYMBOL) + } + } + } - { - let start = "rust_out::test_direct"; - let end = "rust_out::::sound"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); + /// If `def_id` represents a root, pushes it onto the list of + /// outputs. (Note that all roots must be monomorphic.) + fn push_if_root(&mut self, def_id: LocalDefId) { + if self.is_root(def_id) { + trace!("found root"); + + let instance = Instance::mono(self.tcx, def_id.to_def_id()); + self.output.push(( + create_fn_mono_item(self.tcx, instance, DUMMY_SP), + MonomorphizationContext::Root, + )); + } + } + + /// As a special case, when/if we encounter the + /// `main()` function, we also have to generate a + /// monomorphized copy of the start lang item based on + /// the return type of `main`. This is not needed when + /// the user writes their own `start` manually. + fn push_extra_entry_roots(&mut self) { + let Some((main_def_id, EntryFnType::Main { .. })) = self.entry_fn else { + return; + }; + + let Some(start_def_id) = self.tcx.lang_items().start_fn() else { + panic!("Start lang item not found") + }; + let main_ret_ty = self + .tcx + .fn_sig(main_def_id) + .no_bound_vars() + .unwrap() + .output(); + + // Given that `main()` has no arguments, + // then its return type cannot have + // late-bound regions, since late-bound + // regions must appear in the argument + // listing. + let main_ret_ty = self.tcx.normalize_erasing_regions( + ty::ParamEnv::reveal_all(), + main_ret_ty.no_bound_vars().unwrap(), + ); + + let start_instance = Instance::resolve( + self.tcx, + ty::ParamEnv::reveal_all(), + start_def_id, + self.tcx.mk_args(&[main_ret_ty.into()]), + ) + .unwrap() + .unwrap(); + + self.output.push(( + create_fn_mono_item(self.tcx, start_instance, DUMMY_SP), + MonomorphizationContext::Root, + )); + } +} - let start = "rust_out::sound_generic::"; - let end = "rust_out::::sound"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); +fn create_mono_items_for_default_impls<'tcx>( + tcx: TyCtxt<'tcx>, + item: hir::ItemId, + output: &mut MonoItems<'tcx>, +) { + let Some(impl_) = tcx.impl_trait_ref(item.owner_id) else { + return; + }; + + if matches!( + tcx.impl_polarity(impl_.skip_binder().def_id), + ty::ImplPolarity::Negative + ) { + return; + } - let start = "rust_out::sound_dyn"; - let end = "rust_out::Animal::sound".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefDyn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefDyn); + if tcx + .generics_of(item.owner_id) + .own_requires_monomorphization() + { + return; + } - let start = "rust_out::Animal::sound".to_owned() + SUFFIX_DYN; - let end = "rust_out::::sound".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::TraitImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::TraitImpl); + // Lifetimes never affect trait selection, so we are allowed to eagerly + // instantiate an instance of an impl method if the impl (and method, + // which we check below) is only parameterized over lifetime. In that case, + // we use the ReErased, which has no lifetime information associated with + // it, to validate whether or not the impl is legal to instantiate at all. + let only_region_params = |param: &ty::GenericParamDef, _: &_| match param.kind { + GenericParamDefKind::Lifetime => tcx.lifetimes.re_erased.into(), + GenericParamDefKind::Const { + is_host_effect: true, + .. + } => tcx.consts.true_.into(), + GenericParamDefKind::Type { .. } | GenericParamDefKind::Const { .. } => { + unreachable!( + "`own_requires_monomorphization` check means that \ + we should have no type/const params" + ) + } + }; + let impl_args = GenericArgs::for_item(tcx, item.owner_id.to_def_id(), only_region_params); + let trait_ref = impl_.instantiate(tcx, impl_args); + + // Unlike 'lazy' monomorphization that begins by collecting items transitively + // called by `main` or other global items, when eagerly monomorphizing impl + // items, we never actually check that the predicates of this impl are satisfied + // in a empty reveal-all param env (i.e. with no assumptions). + // + // Even though this impl has no type or const generic parameters, because we don't + // consider higher-ranked predicates such as `for<'a> &'a mut [u8]: Copy` to + // be trivially false. We must now check that the impl has no impossible-to-satisfy + // predicates. + if tcx.subst_and_check_impossible_predicates((item.owner_id.to_def_id(), impl_args)) { + return; + } - let start = "rust_out::::walk".to_owned() + SUFFIX_DYN; - let end = "rust_out::::walk"; - assert_contains_edge(&graph, &start, &end, &EdgeType::DynFn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::DynFn); + let param_env = ty::ParamEnv::reveal_all(); + let trait_ref = tcx.normalize_erasing_regions(param_env, trait_ref); + let overridden_methods = tcx.impl_item_implementor_ids(item.owner_id); + for method in tcx.provided_trait_methods(trait_ref.def_id) { + if overridden_methods.contains_key(&method.def_id) { + continue; } + if tcx + .generics_of(method.def_id) + .own_requires_monomorphization() { - let start = "rust_out::test_mut_direct"; - let end = "rust_out::::set_treat"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); - - let start = "rust_out::set_treat_generic::"; - let end = "rust_out::::set_treat"; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefImpl); - - let start = "rust_out::set_treat_dyn"; - let end = "rust_out::Animal::set_treat".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::FnDefDyn); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::FnDefDyn); - - let start = "rust_out::Animal::set_treat".to_owned() + SUFFIX_DYN; - let end = "rust_out::::set_treat".to_owned() + SUFFIX_DYN; - assert_contains_edge(&graph, &start, &end, &EdgeType::TraitImpl); - assert_does_not_contain_edge(&graph, &end, &start, &EdgeType::TraitImpl); + continue; + } + + // As mentioned above, the method is legal to eagerly instantiate if it + // only has lifetime generic parameters. This is validated by + let args = trait_ref + .args + .extend_to(tcx, method.def_id, only_region_params); + let instance = ty::Instance::expect_resolve(tcx, param_env, method.def_id, args); + + let mono_item = create_fn_mono_item(tcx, instance, DUMMY_SP); + if mono_item.node.is_instantiable(tcx) && should_codegen_locally(tcx, &instance) { + output.push((mono_item, MonomorphizationContext::Root)); + } + } +} + +/// Scans the CTFE alloc in order to find function calls, closures, and drop-glue. +fn collect_alloc<'tcx>(tcx: TyCtxt<'tcx>, alloc_id: AllocId, output: &mut MonoItems<'tcx>) { + match tcx.global_alloc(alloc_id) { + GlobalAlloc::Static(def_id) => { + assert!(!tcx.is_thread_local_static(def_id)); + let instance = Instance::mono(tcx, def_id); + if should_codegen_locally(tcx, &instance) { + trace!("collecting static {:?}", def_id); + output.push(( + dummy_spanned(MonoItem::Static(def_id)), + // 5.1. function -> accessed static variable + // 5.2. static variable -> static variable that is pointed to + MonomorphizationContext::Local(EdgeType::Static), + )); + } + } + GlobalAlloc::Memory(alloc) => { + trace!("collecting {:?} with {:#?}", alloc_id, alloc); + for &prov in alloc.inner().provenance().ptrs().values() { + rustc_data_structures::stack::ensure_sufficient_stack(|| { + collect_alloc(tcx, prov.alloc_id(), output); + }); + } + } + GlobalAlloc::Function(fn_instance) => { + if should_codegen_locally(tcx, &fn_instance) { + trace!("collecting {:?} with {:#?}", alloc_id, fn_instance); + output.push(( + create_fn_mono_item(tcx, fn_instance, DUMMY_SP), + // 5.3. static variable -> function that is pointed to + MonomorphizationContext::Local(EdgeType::FnPtr), + )); + // IMPORTANT: This ensures that functions referenced in closures contained in Consts are considered + // For instance this is the case for all test functions + if fn_instance.args.len() > 0 { + let maybe_arg = fn_instance.args.get(0); + let maybe_pointee = maybe_arg.and_then(|arg| arg.as_type()); + + if let Some(pointee) = maybe_pointee { + trace!("collecting function pointer to {:#?}", pointee); + visit_fn_use(tcx, pointee, false, DUMMY_SP, output, EdgeType::FnPtr); + } + } + } + } + GlobalAlloc::VTable(ty, trait_ref) => { + let alloc_id = tcx.vtable_allocation((ty, trait_ref)); + collect_alloc(tcx, alloc_id, output) + } + } +} + +// Source: https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_monomorphize/lib.rs.html#25-46 +fn custom_coerce_unsize_info<'tcx>( + tcx: TyCtxtAt<'tcx>, + source_ty: Ty<'tcx>, + target_ty: Ty<'tcx>, +) -> Result { + let trait_ref = ty::TraitRef::from_lang_item( + tcx.tcx, + LangItem::CoerceUnsized, + tcx.span, + [source_ty, target_ty], + ); + + match tcx.codegen_select_candidate((ty::ParamEnv::reveal_all(), trait_ref)) { + Ok(traits::ImplSource::UserDefined(traits::ImplSourceUserDefinedData { + impl_def_id, + .. + })) => Ok(tcx.coerce_unsized_info(impl_def_id).custom_kind.unwrap()), + impl_source => { + bug!("invalid `CoerceUnsized` impl_source: {:?}", impl_source); } } } diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 6ee912b2..00000000 --- a/src/utils.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Source: https://github.com/lizhuohua/rust-mir-checker/blob/86c3c26e797d3e25a38044fa98b765c5d220e4ea/src/utils.rs - -/// Copied from Miri -/// Returns the "default sysroot" if no `--sysroot` flag is set. -/// Should be a compile-time constant. -pub fn compile_time_sysroot() -> Option { - if option_env!("RUSTC_STAGE").is_some() { - // This is being built as part of rustc, and gets shipped with rustup. - // We can rely on the sysroot computation in librustc. - return None; - } - // For builds outside rustc, we need to ensure that we got a sysroot - // that gets used as a default. The sysroot computation in librustc would - // end up somewhere in the build dir. - // Taken from PR . - let home = option_env!("RUSTUP_HOME").or(option_env!("MULTIRUST_HOME")); - let toolchain = option_env!("RUSTUP_TOOLCHAIN").or(option_env!("MULTIRUST_TOOLCHAIN")); - Some(match (home, toolchain) { - (Some(home), Some(toolchain)) => format!("{}/toolchains/{}", home, toolchain), - _ => option_env!("RUST_SYSROOT") - .expect("To build Miri without rustup, set the `RUST_SYSROOT` env var at build time") - .to_owned(), - }) -} diff --git a/test-data/blackbox/adt/Cargo.toml b/test-data/blackbox/adt/Cargo.toml new file mode 100644 index 00000000..c801a9a2 --- /dev/null +++ b/test-data/blackbox/adt/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "adt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +changes_display = [] +changes_debug = [] +changes_drop = [] + +[dependencies] diff --git a/test-data/blackbox/adt/src/lib.rs b/test-data/blackbox/adt/src/lib.rs new file mode 100644 index 00000000..3fff689f --- /dev/null +++ b/test-data/blackbox/adt/src/lib.rs @@ -0,0 +1,112 @@ +#![allow(dead_code)] +use std::fmt::{Debug, Display, Write}; + +struct Foo { + data: T, +} + +impl Display for Foo { + #[cfg(not(feature = "changes_display"))] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Foo: {}", self.data)) + } + + #[cfg(feature = "changes_display")] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Unexpected: {}", self.data)) + } +} + +impl Debug for Foo { + #[cfg(not(feature = "changes_debug"))] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Foo: {}", self.data)) + } + + #[cfg(feature = "changes_debug")] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Unexpected: {}", self.data)) + } +} + +static mut DROPPED: bool = false; + +impl Drop for Foo { + #[cfg(not(feature = "changes_drop"))] + fn drop(&mut self) { + unsafe { DROPPED = true }; + } + + #[cfg(feature = "changes_drop")] + fn drop(&mut self) {} +} + +fn generic_display(s: &S, buf: &mut impl Write) { + buf.write_fmt(format_args!("{}", s)).unwrap(); +} + +trait DynFoo { + type Ty; + + fn data(&self) -> &T; + fn type_data(&self) -> Self::Ty; +} + +impl DynFoo for Foo { + fn data(&self) -> &i32 { + &self.data + } + + type Ty = u32; + + fn type_data(&self) -> Self::Ty { + self.data as u32 + } +} + +impl Display for dyn DynFoo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "signed: {}, unsigned: {}", + self.data(), + self.type_data() + )) + } +} + +#[cfg(test)] +pub mod test { + use crate::*; + + #[test] + fn test_display() { + { + let instance: Foo = Foo { data: 1 }; + assert_eq!(format!("{}", instance), "Foo: 1"); + } + assert!(unsafe { DROPPED }); + } + + #[test] + fn test_debug() { + let instance: Foo = Foo { data: 1 }; + assert_eq!(format!("{:?}", instance), "Foo: 1"); + } + + #[test] + fn test_generic() { + let mut buf = String::new(); + let instance: Foo = Foo { data: 1 }; + generic_display(&instance, &mut buf); + assert_eq!(buf, "Foo: 1") + } + + #[test] + fn test_dyn() { + let dyn_instance: Box> = Box::new(Foo { data: -1 }); + assert_eq!( + format!("{}", dyn_instance), + "signed: -1, unsigned: 4294967295" + ); + } +} diff --git a/test-data/manual/allocator/Cargo.toml b/test-data/blackbox/allocator/Cargo.toml similarity index 100% rename from test-data/manual/allocator/Cargo.toml rename to test-data/blackbox/allocator/Cargo.toml diff --git a/test-data/manual/allocator/src/lib.rs b/test-data/blackbox/allocator/src/lib.rs similarity index 100% rename from test-data/manual/allocator/src/lib.rs rename to test-data/blackbox/allocator/src/lib.rs diff --git a/test-data/blackbox/assoc_items/Cargo.toml b/test-data/blackbox/assoc_items/Cargo.toml new file mode 100644 index 00000000..4106bbda --- /dev/null +++ b/test-data/blackbox/assoc_items/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "assoc_items" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +changes_string = [] +changes_assoc_const = [] +changes_assoc_type = [] + +[dependencies] diff --git a/test-data/blackbox/assoc_items/src/main.rs b/test-data/blackbox/assoc_items/src/main.rs new file mode 100644 index 00000000..8f4ef567 --- /dev/null +++ b/test-data/blackbox/assoc_items/src/main.rs @@ -0,0 +1,68 @@ +trait CustomTrait { + #[cfg(not(feature = "changes_assoc_const"))] + const FOO: i32 = 42; + + #[cfg(feature = "changes_assoc_const")] + const FOO: i32 = 21; + + type TYPE; + + fn get(self) -> T; + + fn value() -> i32 { + Self::FOO + } + + fn ty() -> &'static str { + std::any::type_name::() + } +} + +impl<'a> CustomTrait for String { + #[cfg(not(feature = "changes_assoc_type"))] + type TYPE = i16; + + #[cfg(feature = "changes_assoc_type")] + type TYPE = f32; + + #[cfg(not(feature = "changes_string"))] + fn get(self) -> String { + self + } + + #[cfg(feature = "changes_string")] + fn get(self) -> String { + "".to_string() + } +} + +impl CustomTrait for i32 { + type TYPE = f32; + + fn get(self) -> u32 { + self as u32 + } +} + +fn main() { + println!("Hello, world!"); +} + +#[test] +fn test_call() { + let string = "Test".to_string(); + assert_eq!(string.clone().get(), string.clone()); + + let signed = 42; + assert_eq!(signed.get(), signed as u32); +} + +#[test] +fn test_assoc_const() { + assert_eq!(>::value(), 42); +} + +#[test] +fn test_assoc_type() { + assert_eq!(>::ty(), "i16"); +} diff --git a/test-data/manual/adt/Cargo.toml b/test-data/blackbox/blanket_impl/Cargo.toml similarity index 75% rename from test-data/manual/adt/Cargo.toml rename to test-data/blackbox/blanket_impl/Cargo.toml index dca19553..9e55482f 100644 --- a/test-data/manual/adt/Cargo.toml +++ b/test-data/blackbox/blanket_impl/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "adt" +name = "blanket_impl" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +changes_inner = [] + [dependencies] diff --git a/test-data/blackbox/blanket_impl/src/main.rs b/test-data/blackbox/blanket_impl/src/main.rs new file mode 100644 index 00000000..96bab3a6 --- /dev/null +++ b/test-data/blackbox/blanket_impl/src/main.rs @@ -0,0 +1,64 @@ +trait ATrait { + fn fun_a(&self) -> i32; +} + +trait BTrait { + fn fun_b(self) -> i32; +} + +trait CTrait { + fn fun_c(self) -> i32; +} + +impl ATrait for T +where + for<'a> &'a T: CTrait, +{ + fn fun_a(&self) -> i32 { + BTrait::fun_b(self) + } +} + +impl BTrait for &T +where + for<'a> &'a T: CTrait, +{ + fn fun_b(self) -> i32 { + CTrait::fun_c(self) + } +} + +impl CTrait for &AStruct { + fn fun_c(self) -> i32 { + fun() + } +} + +#[cfg(not(feature = "changes_inner"))] +fn fun() -> i32 { + 42 +} +#[cfg(feature = "changes_inner")] +#[inline(never)] +fn fun() -> i32 { + 41 +} + +struct AStruct(); + +#[test] +fn test() { + let s = AStruct {}; + let d: &dyn ATrait = &s; + assert_eq!(d.fun_a(), 42); +} + +#[test] +fn another_test() { + let s = AStruct {}; + assert_eq!(s.fun_a(), 42); +} + +fn main() { + println!("Hello, world!"); +} diff --git a/test-data/manual/primitive/Cargo.toml b/test-data/blackbox/check_same_crate_id/Cargo.toml similarity index 84% rename from test-data/manual/primitive/Cargo.toml rename to test-data/blackbox/check_same_crate_id/Cargo.toml index a4e6117d..d6764945 100644 --- a/test-data/manual/primitive/Cargo.toml +++ b/test-data/blackbox/check_same_crate_id/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "primitive" +name = "check_same_crate_id" version = "0.1.0" edition = "2021" diff --git a/test-data/blackbox/check_same_crate_id/src/lib.rs b/test-data/blackbox/check_same_crate_id/src/lib.rs new file mode 100644 index 00000000..e30020f4 --- /dev/null +++ b/test-data/blackbox/check_same_crate_id/src/lib.rs @@ -0,0 +1,4 @@ +#[test] +pub fn test() { + panic!() +} diff --git a/test-data/manual/command/Cargo.toml b/test-data/blackbox/command/Cargo.toml similarity index 87% rename from test-data/manual/command/Cargo.toml rename to test-data/blackbox/command/Cargo.toml index 3c3824b0..3d782942 100644 --- a/test-data/manual/command/Cargo.toml +++ b/test-data/blackbox/command/Cargo.toml @@ -11,4 +11,6 @@ name = "foo" [[bin]] name = "bar" +[features] +changes_return = [] [dependencies] diff --git a/test-data/manual/command/src/bin/bar.rs b/test-data/blackbox/command/src/bin/bar.rs similarity index 64% rename from test-data/manual/command/src/bin/bar.rs rename to test-data/blackbox/command/src/bin/bar.rs index 98a0bc25..72962ff3 100644 --- a/test-data/manual/command/src/bin/bar.rs +++ b/test-data/blackbox/command/src/bin/bar.rs @@ -8,7 +8,10 @@ pub fn main() -> ExitCode { process::{Command, ExitCode}, }; - let path = PathBuf::from("target_dynamic/debug/foo"); + let mut path = + PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string())); + path.push("debug"); + path.push("foo"); let status = Command::new(path).status().unwrap(); return ExitCode::from(status.code().unwrap() as u8); diff --git a/test-data/manual/command/src/bin/foo.rs b/test-data/blackbox/command/src/bin/foo.rs similarity index 100% rename from test-data/manual/command/src/bin/foo.rs rename to test-data/blackbox/command/src/bin/foo.rs diff --git a/test-data/manual/command/src/lib.rs b/test-data/blackbox/command/src/lib.rs similarity index 66% rename from test-data/manual/command/src/lib.rs rename to test-data/blackbox/command/src/lib.rs index 80f15395..8c95f850 100644 --- a/test-data/manual/command/src/lib.rs +++ b/test-data/blackbox/command/src/lib.rs @@ -2,6 +2,13 @@ // The purpose of this crate is, to ensure that dynamic RustyRTS collects traces of child processes // This function has to be present in the traces of both tests foo and bar + +#[cfg(not(feature = "changes_return"))] pub fn library_fn() -> u8 { 42 } + +#[cfg(feature = "changes_return")] +pub fn library_fn() -> u8 { + 255 +} diff --git a/test-data/manual/command/tests/mod.rs b/test-data/blackbox/command/tests/mod.rs similarity index 100% rename from test-data/manual/command/tests/mod.rs rename to test-data/blackbox/command/tests/mod.rs diff --git a/test-data/blackbox/derive/Cargo.toml b/test-data/blackbox/derive/Cargo.toml new file mode 100644 index 00000000..e18b1fe9 --- /dev/null +++ b/test-data/blackbox/derive/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "derive" +version = "0.1.0" +edition = "2021" + +[features] +changes_debug = [] +changes_hash = [] + +[dependencies] diff --git a/test-data/blackbox/derive/src/lib.rs b/test-data/blackbox/derive/src/lib.rs new file mode 100644 index 00000000..228c73d9 --- /dev/null +++ b/test-data/blackbox/derive/src/lib.rs @@ -0,0 +1,69 @@ +#![allow(dead_code)] + +// The purpose of this crate is, to verify that static RustyRTS can handle calls to functions via indirection over std +// This ensures that the custom sysroot used by static rustyrts is working correctly + +use std::hash::{Hash, Hasher}; +use std::{fmt::Debug, hash::DefaultHasher}; + +struct Inner { + data: u32, +} + +#[derive(Debug, Hash)] +struct Outer { + data: Inner, +} + +impl Debug for Inner { + #[cfg(not(feature = "changes_debug"))] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Inner") + } + + #[cfg(feature = "changes_debug")] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Unexpected") + } +} + +impl Hash for Inner { + fn hash(&self, state: &mut H) { + state.write("Foo".as_bytes()); + + #[cfg(not(feature = "changes_hash"))] + state.write_u32(self.data); + } +} + +fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_debug() { + let sut = Outer { + data: Inner { data: 1 }, + }; + assert_eq!(format!("{:?}", sut), "Outer { data: Inner }") + } + + #[test] + fn test_hash() { + let sut1 = Outer { + data: Inner { data: 1 }, + }; + + let sut2 = Outer { + data: Inner { data: 2 }, + }; + + assert_ne!(calculate_hash(&sut1), calculate_hash(&sut2)); + } +} diff --git a/test-data/blackbox/dynamic/Cargo.toml b/test-data/blackbox/dynamic/Cargo.toml new file mode 100644 index 00000000..8c6fb2d4 --- /dev/null +++ b/test-data/blackbox/dynamic/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "dynamic" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +changes_direct = [] +changes_indirect = [] +changes_removed= [] +changes_static= [] + +[dependencies] diff --git a/test-data/manual/dynamic/src/main.rs b/test-data/blackbox/dynamic/src/main.rs similarity index 54% rename from test-data/manual/dynamic/src/main.rs rename to test-data/blackbox/dynamic/src/main.rs index 871d0d30..f23cc3d4 100644 --- a/test-data/manual/dynamic/src/main.rs +++ b/test-data/blackbox/dynamic/src/main.rs @@ -10,7 +10,7 @@ pub trait Foo { pub trait Bar: Foo { fn bar(&self) -> i32 { - return 42; + return 41; } } @@ -22,35 +22,45 @@ pub trait Baz: Bar { pub struct ImplFoo {} +pub struct ImplBar {} + pub struct ImplBaz {} //############# -// TODO: When any of these four functions, that are called via dynamic dispatch, are commented in or out, -// test_dyn will fail and has to be recognized as affected +// When any of these four functions, that are called via dynamic dispatch, are included, +// test_dyn_added will fail and has to be recognized as affected impl Foo for ImplFoo { - //fn foo(&self) -> i32 { - // return 41; - //} + #[cfg(feature = "changes_direct")] + fn foo(&self) -> i32 { + return 41; + } } impl Foo for ImplBaz { - //fn foo(&self) -> i32 { - // return 41; - //} + #[cfg(feature = "changes_indirect")] + fn foo(&self) -> i32 { + return 41; + } } -impl Bar for T { - //fn bar(&self) -> i32 { - // return 41; - //} +// If this is included, also test_static will fail and should be affected +impl Baz for ImplBaz { + #[cfg(feature = "changes_static")] + fn baz(&self) -> i32 { + return 41; + } } -// If this is uncommented, also test_static will fail and should be affected -impl Baz for ImplBaz { - //fn baz(&self) -> i32 { - // return 41; - //} +//############# +// When this function, that is called via dynamic dispatch, is excluded, +// test_dyn_removed will fail and has to be recognized as affected + +impl Bar for T { + #[cfg(not(feature = "changes_removed"))] + fn bar(&self) -> i32 { + return 42; + } } fn main() { @@ -58,21 +68,25 @@ fn main() { } #[test] -pub fn test_dyn() { +pub fn test_dyn_added() { let bar: &dyn Bar = &ImplFoo {}; let foo: &dyn Foo = bar; // Up-casting from Bar to Foo (only possible with special compiler feature) - assert_eq!(bar.foo(), 42); - assert_eq!(foo.foo(), 42); - assert_eq!(bar.bar(), 42); let baz: &dyn Baz = &ImplBaz {}; assert_eq!(baz.foo(), 42); - assert_eq!(baz.bar(), 42); assert_eq!(baz.baz(), 42); } +#[test] +pub fn test_dyn_removed() { + let bar: &dyn Bar = &ImplFoo {}; + + assert_eq!(bar.foo(), 42); + assert_eq!(bar.bar(), 42); +} + #[test] pub fn test_static() { let impl_baz = ImplBaz {}; diff --git a/test-data/blackbox/fn_ptr/Cargo.toml b/test-data/blackbox/fn_ptr/Cargo.toml new file mode 100644 index 00000000..170ec318 --- /dev/null +++ b/test-data/blackbox/fn_ptr/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fn_ptr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +changes_fn = [] +changes_static = [] + +test_direct = [] +test_indirect = [] + +[dependencies] diff --git a/test-data/blackbox/fn_ptr/src/main.rs b/test-data/blackbox/fn_ptr/src/main.rs new file mode 100644 index 00000000..c15178e0 --- /dev/null +++ b/test-data/blackbox/fn_ptr/src/main.rs @@ -0,0 +1,46 @@ +#[cfg(not(feature = "changes_static"))] +static CALLBACK: Callback = Callback { func: foo }; + +#[cfg(feature = "changes_static")] +static CALLBACK: Callback = Callback { func: bar }; + +static CALLBACK_INDIRECT: Callback = Callback { + func: CALLBACK.func, +}; + +struct Callback { + func: fn() -> i32, +} + +#[cfg(not(feature = "changes_fn"))] +fn foo() -> i32 { + 42 +} + +#[cfg(feature = "changes_fn")] +fn foo() -> i32 { + 41 +} + +#[allow(dead_code)] +fn bar() -> i32 { + 43 +} + +fn main() { + println!("Hello, world!"); +} + +#[cfg(feature = "test_direct")] +#[test] +fn test_direct() { + println!("{}", (CALLBACK.func)()); + assert_eq!((CALLBACK.func)(), 42); +} + +#[cfg(feature = "test_indirect")] +#[test] +fn test_indirect() { + println!("{}", (CALLBACK_INDIRECT.func)()); + assert_eq!((CALLBACK_INDIRECT.func)(), 42); +} diff --git a/test-data/manual/lazy/Cargo.toml b/test-data/blackbox/lazy/Cargo.toml similarity index 86% rename from test-data/manual/lazy/Cargo.toml rename to test-data/blackbox/lazy/Cargo.toml index 259ff010..1c84ea8a 100644 --- a/test-data/manual/lazy/Cargo.toml +++ b/test-data/blackbox/lazy/Cargo.toml @@ -5,5 +5,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +changes_lazy = [] + [dependencies] lazy_static = "1.4.0" diff --git a/test-data/manual/lazy/src/main.rs b/test-data/blackbox/lazy/src/main.rs similarity index 78% rename from test-data/manual/lazy/src/main.rs rename to test-data/blackbox/lazy/src/main.rs index 960c6f5e..6d3a9150 100644 --- a/test-data/manual/lazy/src/main.rs +++ b/test-data/blackbox/lazy/src/main.rs @@ -2,11 +2,16 @@ use lazy_static::lazy_static; // The purpose of this crate is, to verify that RustyRTS can handle variables that are initialized lazily -// TODO: change the argument +#[cfg(not(feature = "changes_lazy"))] lazy_static! { static ref VAR: usize = value(2); } +#[cfg(feature = "changes_lazy")] +lazy_static! { + static ref VAR: usize = value(42); +} + fn value(input: usize) -> usize { input * 10 } diff --git a/test-data/blackbox/static_var/Cargo.toml b/test-data/blackbox/static_var/Cargo.toml new file mode 100644 index 00000000..b13b796b --- /dev/null +++ b/test-data/blackbox/static_var/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "static_var" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +changes_immutable = [] +changes_mutable = [] + +[dependencies] diff --git a/test-data/blackbox/static_var/src/lib.rs b/test-data/blackbox/static_var/src/lib.rs new file mode 100644 index 00000000..21ccd88e --- /dev/null +++ b/test-data/blackbox/static_var/src/lib.rs @@ -0,0 +1,27 @@ +pub const fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(not(feature = "changes_immutable"))] +static IMMUTABLE: usize = add(1, 2); + +#[cfg(feature = "changes_immutable")] +static IMMUTABLE: usize = add(2, 2); + +#[cfg(not(feature = "changes_mutable"))] +static mut MUTABLE: usize = add(3, 4); + +#[cfg(feature = "changes_mutable")] +static mut MUTABLE: usize = add(4, 4); + +#[test] +fn test_immutable() { + assert_eq!(IMMUTABLE, 3); +} + +#[test] +fn test_mutable() { + assert_eq!(unsafe { MUTABLE }, 7); + unsafe { MUTABLE = 42 }; + assert_eq!(unsafe { MUTABLE }, 42); +} diff --git a/test-data/manual/threading/.gitignore b/test-data/blackbox/threading/.gitignore similarity index 100% rename from test-data/manual/threading/.gitignore rename to test-data/blackbox/threading/.gitignore diff --git a/test-data/manual/threading/Cargo.toml b/test-data/blackbox/threading/Cargo.toml similarity index 70% rename from test-data/manual/threading/Cargo.toml rename to test-data/blackbox/threading/Cargo.toml index bdb9d765..2042c36e 100644 --- a/test-data/manual/threading/Cargo.toml +++ b/test-data/blackbox/threading/Cargo.toml @@ -5,5 +5,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +test1_panic = [] +test2_panic = [] +changes_test1 = [] +changes_test2 = [] + [dependencies] threadpool = "1.8.1" diff --git a/test-data/manual/threading/src/main.rs b/test-data/blackbox/threading/src/main.rs similarity index 78% rename from test-data/manual/threading/src/main.rs rename to test-data/blackbox/threading/src/main.rs index 5a852203..4d8cdccc 100644 --- a/test-data/manual/threading/src/main.rs +++ b/test-data/blackbox/threading/src/main.rs @@ -5,13 +5,12 @@ use threadpool::ThreadPool; // The purpose of this crate is, to demonstrate why it is necessary to execute tests in separate processes // in dynamic RustyRTS -// The traces of test should not contain test2 and vice versa +// The traces of test1 should not contain test2 and vice versa fn main() {} -// This is the `main` thread #[test] -fn test() { +fn test1() { // source: https://doc.rust-lang.org/rust-by-example/std_misc/threads.html const NTHREADS: u32 = 10; @@ -23,6 +22,9 @@ fn test() { // Spin up another thread children.push(thread::spawn(move || { println!("this is thread number {}", i); + + #[cfg(feature = "changes_test1")] + println!("Unexpected"); })); } @@ -30,6 +32,9 @@ fn test() { // Wait for the thread to finish. Returns a result. let _ = child.join(); } + + #[cfg(feature = "test1_panic")] + panic!(); } #[test] @@ -46,8 +51,14 @@ fn test2() { pool.execute(move || { tx.send(1) .expect("channel will be there waiting for the pool"); + + #[cfg(feature = "changes_test2")] + println!("Unexpected"); }); } assert_eq!(rx.iter().take(n_jobs).fold(0, |a, b| a + b), 8); + + #[cfg(feature = "test2_panic")] + panic!(); } diff --git a/test-data/blackbox/unused_lifetime/.gitignore b/test-data/blackbox/unused_lifetime/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/test-data/blackbox/unused_lifetime/.gitignore @@ -0,0 +1 @@ +/target diff --git a/test-data/manual/dynamic/Cargo.toml b/test-data/blackbox/unused_lifetime/Cargo.toml similarity index 73% rename from test-data/manual/dynamic/Cargo.toml rename to test-data/blackbox/unused_lifetime/Cargo.toml index 77d624d0..c808b840 100644 --- a/test-data/manual/dynamic/Cargo.toml +++ b/test-data/blackbox/unused_lifetime/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "dynamic" +name = "unused_lifetime" version = "0.1.0" edition = "2021" +[features] +changes_unused = [] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/test-data/blackbox/unused_lifetime/src/main.rs b/test-data/blackbox/unused_lifetime/src/main.rs new file mode 100644 index 00000000..9900002f --- /dev/null +++ b/test-data/blackbox/unused_lifetime/src/main.rs @@ -0,0 +1,43 @@ +use std::fmt::Display; + +struct Foo where T: Display{ + data1: T, + data2: T +} + +impl Foo where T: Display{ + + #[cfg(not(feature = "changes_unused"))] + fn data(&self) -> &T { + &self.data1 + } + + #[cfg(feature = "changes_unused")] + fn data(&self) -> &T { + &self.data2 + } +} + +// Not sure if this is a bug... +// The 'unused lifetime here is not referred to anywhere else +// Still, it is part of the generic args of the drop function +// +// This test checks if rustyrts incorporates the lifetime properly +// If not, rustyrts crashes + +impl<'unused, T> Drop for Foo where T: Display{ + fn drop(&mut self) { + println!("Dropped: {}", self.data()); + } +} + + +#[test] +fn test() { + let foo = Foo{data1: 1, data2: 2}; + assert_eq!(*foo.data(), 1); +} + +fn main() { + +} diff --git a/test-data/manual/adt/src/lib.rs b/test-data/manual/adt/src/lib.rs deleted file mode 100644 index 58079302..00000000 --- a/test-data/manual/adt/src/lib.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::fmt::{Debug, Display, Write}; - -struct Foo { - data: T, -} - -impl Display for Foo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("Foo: {}", self.data)) - } -} - -impl Debug for Foo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("Foo: {}", self.data)) - } -} - -impl Drop for Foo { - fn drop(&mut self) { - println!("Dropped") - } -} - -fn generic_display(s: &S) { - println!("{}", s); -} - -trait DynFoo { - type Ty; - - fn data(&self) -> &T; - fn type_data(&self) -> Self::Ty; -} - -impl DynFoo for Foo { - fn data(&self) -> &i32 { - &self.data - } - - type Ty = u32; - - fn type_data(&self) -> Self::Ty { - self.data as u32 - } -} - -impl Display for dyn DynFoo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("signed: {}", self.data()))?; - f.write_str(&format!("unsigned: {}", self.type_data())) - } -} - -#[test] -fn test_display() { - let mut instance: Foo = Foo { data: 1 }; - print!("{}", instance); - assert_eq!(instance.data, 1); -} - -#[test] -fn test_debug() { - let mut instance: Foo = Foo { data: 1 }; - print!("{:?}", instance); - assert_eq!(instance.data, 1); -} - -#[test] -fn test_generic() { - let mut instance: Foo = Foo { data: 1 }; - generic_display(&instance); - assert_eq!(instance.data, 1); -} - -#[test] -fn test_dyn() { - let mut dyn_instance: Box> = Box::new(Foo { data: 1 }); - print!("{}", dyn_instance); -} diff --git a/test-data/manual/derive/Cargo.toml b/test-data/manual/derive/Cargo.toml index 841cb9bd..b7d3c1e8 100644 --- a/test-data/manual/derive/Cargo.toml +++ b/test-data/manual/derive/Cargo.toml @@ -5,7 +5,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] - [dependencies] -proc_macro_derive = {path="../proc_macro_derive"} \ No newline at end of file +proc_macro_derive = {path="../proc_macro_derive"} diff --git a/test-data/manual/derive/src/lib.rs b/test-data/manual/derive/src/main.rs similarity index 90% rename from test-data/manual/derive/src/lib.rs rename to test-data/manual/derive/src/main.rs index 1f2d3fcf..8699a93b 100644 --- a/test-data/manual/derive/src/lib.rs +++ b/test-data/manual/derive/src/main.rs @@ -13,6 +13,10 @@ fn echoed() -> u32 { #[derive(Echo)] pub struct Foo; +pub fn main() { + println!("{}", Foo::echo()); +} + #[cfg(test)] mod test { use super::*; diff --git a/test-data/manual/fmt/src/main.rs b/test-data/manual/fmt/src/main.rs deleted file mode 100644 index bd6c6728..00000000 --- a/test-data/manual/fmt/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fmt::Display; - -struct Foo; -struct Bar; -struct Baz; - - -impl Display for Foo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Foo") - } -} - - -impl Display for Bar { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Bar") - } -} - -impl Display for Baz { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Bar") - } -} - - -fn main() { - println!("Hello, world!"); -} - -#[test] -fn test_foo() { - assert_eq!(format!("{}", Foo), "Foo") -} - -#[test] -fn test_bar() { - assert_eq!(format!("{}", Bar), "Bar") -} - -#[test] -fn test_baz() { - assert_eq!(format!("{}", Baz), "Baz") -} diff --git a/test-data/manual/fmt/Cargo.toml b/test-data/manual/mapper/Cargo.toml similarity index 90% rename from test-data/manual/fmt/Cargo.toml rename to test-data/manual/mapper/Cargo.toml index 8f176385..4ead8395 100644 --- a/test-data/manual/fmt/Cargo.toml +++ b/test-data/manual/mapper/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fmt" +name = "mapper" version = "0.1.0" edition = "2021" diff --git a/test-data/manual/mapper/src/main.rs b/test-data/manual/mapper/src/main.rs new file mode 100644 index 00000000..b41c9766 --- /dev/null +++ b/test-data/manual/mapper/src/main.rs @@ -0,0 +1,25 @@ +fn main() { + println!("Hello, world!"); +} + +#[test] +fn test1() { + let map1 = |input| input + 1; + + let ints = Vec::from(&[1, 2, 3, 4]); + let actual: Vec = ints.iter().map(map1).collect(); + let expected = Vec::from(&[2, 3, 4, 5]); + + assert_eq!(actual, expected); +} + +#[test] +fn test2() { + let map2 = |input| input * input; + + let ints = Vec::from(&[1, 2, 3, 4]); + let actual: Vec = ints.iter().map(map2).collect(); + let expected = Vec::from(&[1, 4, 9, 16]); + + assert_eq!(actual, expected); +} diff --git a/test-data/manual/primitive/src/main.rs b/test-data/manual/primitive/src/main.rs deleted file mode 100644 index 96ff7908..00000000 --- a/test-data/manual/primitive/src/main.rs +++ /dev/null @@ -1,54 +0,0 @@ -trait CustomTrait { - const FOO: i32 = 42; - - fn get(self) -> T; - - fn value() -> i32 { - Self::FOO - } -} - -impl<'a> CustomTrait<&'a String> for &'a String { - fn get(self) -> &'a String { - println!("Test"); - &self - } - - fn value() -> i32 { - 21 - } -} - -impl<'a> CustomTrait<&'a str> for &'a str { - fn get(self) -> &'a str { - &self - } -} - -impl CustomTrait for i32 { - fn get(self) -> u32 { - println!("Test"); - self as u32 - } -} - -fn main() { - println!("Hello, world!"); -} - -#[test] -fn test_primitive() { - let string = "Test".to_string(); - assert_eq!(*string.get(), string); - - let str_slice = &string; - assert_eq!(*str_slice.get(), string); - - let signed = 42; - assert_eq!(signed.get(), signed as u32); -} - -#[test] -fn test_assoc_const() { - assert_eq!(>::value(), 42); -} diff --git a/test-data/manual/proc_macro_derive/Cargo.toml b/test-data/manual/proc_macro_derive/Cargo.toml index 2476b900..6e16a9aa 100644 --- a/test-data/manual/proc_macro_derive/Cargo.toml +++ b/test-data/manual/proc_macro_derive/Cargo.toml @@ -12,4 +12,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0", features = ["full"] } \ No newline at end of file +syn = { version = "1.0", features = ["full"] } +trybuild = "1.0.84" \ No newline at end of file diff --git a/test-data/manual/proc_macro_derive/tests/lib.rs b/test-data/manual/proc_macro_derive/tests/lib.rs new file mode 100644 index 00000000..e76b1d0a --- /dev/null +++ b/test-data/manual/proc_macro_derive/tests/lib.rs @@ -0,0 +1,5 @@ +#[test] +pub fn trybuild_test() { + let t = trybuild::TestCases::new(); + t.pass("../derive/src/main.rs"); +} diff --git a/tests/blackbox.rs b/tests/blackbox.rs new file mode 100644 index 00000000..da6f46ee --- /dev/null +++ b/tests/blackbox.rs @@ -0,0 +1,212 @@ +use lazy_static::lazy_static; +use std::{fs::create_dir_all, path::PathBuf}; +use std::{path::Path, process::Command}; +use test_case::test_case; + +use rustyrts::constants::{ENV_BLACKBOX_TEST, ENV_TARGET_DIR}; +use tempdir::TempDir; + +enum Mode { + Dynamic, + Static, +} + +impl Into<&str> for &Mode { + fn into(self) -> &'static str { + match self { + Mode::Dynamic => "dynamic", + Mode::Static => "static", + } + } +} + +fn command(mode: &Mode, dir: &PathBuf, target_dir: &Path, feature: Option<&str>) -> Command { + let mut ret = Command::new(env!("CARGO_BIN_EXE_cargo-rustyrts")); + ret.arg("rustyrts").arg(Into::<&str>::into(mode)); + ret.current_dir(dir); + + if let Some(name) = feature { + ret.arg("--") + .arg("--features") + .arg(name) + .arg("--") + .arg("--") + .arg("--features") + .arg(name); + } + + ret.env(ENV_TARGET_DIR, target_dir); + ret.env(ENV_BLACKBOX_TEST, "true"); + + ret +} + +lazy_static! { + static ref PATH: PathBuf = { + let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path_buf.push("test-data"); + path_buf.push("blackbox"); + path_buf + }; +} + +#[test_case(Mode::Dynamic; "dynamic_check_same_crate_id")] +#[test_case(Mode::Static; "static_check_same_crate_id")] +fn check_same_crate_id(mode: Mode) { + let mut dir = PATH.clone(); + dir.push("check_same_crate_id"); + + let target_dir = TempDir::new_in( + env!("CARGO_TARGET_TMPDIR"), + dir.file_name().unwrap().to_str().unwrap(), + ) + .unwrap(); + + { + println!("-------- baseline --------"); + let result = command(&mode, &dir, target_dir.path(), None) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(!result.status.success()); + } + + { + println!("-------- with changes --------"); + let result = command(&mode, &dir, target_dir.path(), None) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(result.status.success()); + } +} + +#[test_case(Mode::Dynamic, "adt", "", "changes_display")] +#[test_case(Mode::Static, "adt", "", "changes_display")] +#[test_case(Mode::Dynamic, "adt", "", "changes_debug")] +#[test_case(Mode::Static, "adt", "", "changes_debug")] +#[test_case(Mode::Dynamic, "adt", "", "changes_drop")] +#[test_case(Mode::Static, "adt", "", "changes_drop")] +#[test_case(Mode::Dynamic, "command", "", "changes_return")] +#[test_case(Mode::Dynamic, "dynamic", "", "changes_direct")] +#[test_case(Mode::Static, "dynamic", "", "changes_direct")] +#[test_case(Mode::Dynamic, "dynamic", "", "changes_indirect")] +#[test_case(Mode::Static, "dynamic", "", "changes_indirect")] +#[test_case(Mode::Dynamic, "dynamic", "", "changes_generic")] +#[test_case(Mode::Static, "dynamic", "", "changes_generic")] +#[test_case(Mode::Dynamic, "dynamic", "", "changes_static")] +#[test_case(Mode::Static, "dynamic", "", "changes_static")] +#[test_case(Mode::Dynamic, "dynamic", "", "changes_removed")] +#[test_case(Mode::Static, "dynamic", "", "changes_removed")] +#[test_case(Mode::Dynamic, "assoc_items", "", "changes_string")] +#[test_case(Mode::Static, "assoc_items", "", "changes_string")] +#[test_case(Mode::Dynamic, "assoc_items", "", "changes_assoc_const")] +#[test_case(Mode::Static, "assoc_items", "", "changes_assoc_const")] +// #[test_case(Mode::Dynamic, "assoc_items", "", "changes_assoc_type")] // Does not work yet +// #[test_case(Mode::Static, "assoc_items", "", "changes_assoc_type")] +#[test_case(Mode::Dynamic, "lazy", "", "changes_lazy")] +#[test_case(Mode::Static, "lazy", "", "changes_lazy")] +#[test_case(Mode::Dynamic, "static_var", "", "changes_immutable")] +#[test_case(Mode::Static, "static_var", "", "changes_mutable")] +#[test_case(Mode::Dynamic, "fn_ptr", "test_direct", "test_direct,changes_fn")] +#[test_case(Mode::Static, "fn_ptr", "test_direct", "test_direct,changes_fn")] +#[test_case(Mode::Dynamic, "fn_ptr", "test_direct", "test_direct,changes_static")] +#[test_case(Mode::Static, "fn_ptr", "test_direct", "test_direct,changes_static")] +#[test_case(Mode::Dynamic, "fn_ptr", "test_indirect", "test_indirect,changes_fn")] +#[test_case(Mode::Static, "fn_ptr", "test_indirect", "test_indirect,changes_fn")] +#[test_case(Mode::Dynamic, "derive", "", "changes_debug")] +#[test_case(Mode::Static, "derive", "", "changes_debug")] +#[test_case(Mode::Dynamic, "derive", "", "changes_hash")] +#[test_case(Mode::Static, "derive", "", "changes_hash")] +#[test_case(Mode::Dynamic, "blanket_impl", "", "changes_inner")] +#[test_case(Mode::Static, "blanket_impl", "", "changes_inner")] +#[test_case( + Mode::Dynamic, + "fn_ptr", + "test_indirect", + "test_indirect,changes_static" +)] +#[test_case( + Mode::Static, + "fn_ptr", + "test_indirect", + "test_indirect,changes_static" +)] +#[test_case(Mode::Dynamic, "unused_lifetime", "", "changes_unused")] +#[test_case(Mode::Static, "unused_lifetime", "", "changes_unused")] +fn blackbox_test_affected(mode: Mode, name: &str, features_baseline: &str, features_changes: &str) { + let mut dir = PATH.clone(); + dir.push(name); + + let target_dir = TempDir::new_in( + env!("CARGO_TARGET_TMPDIR"), + dir.file_name().unwrap().to_str().unwrap(), + ) + .unwrap(); + + { + println!("-------- baseline --------"); + let result = command(&mode, &dir, target_dir.path(), Some(features_baseline)) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(result.status.success()); + } + + { + println!("-------- with changes --------"); + let result = command(&mode, &dir, target_dir.path(), Some(features_changes)) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(!result.status.success()); + } +} + +#[test_case( + Mode::Dynamic, + "threading", + "test1_panic", + "test1_panic, changes_test2" +)] +#[test_case(Mode::Dynamic, "threading", "test2_panic", "test2_panic, changes_test1")] +fn blackbox_test_not_affected( + mode: Mode, + name: &str, + features_baseline: &str, + features_changes: &str, +) { + let mut dir = PATH.clone(); + dir.push(name); + + let target_dir = TempDir::new_in( + env!("CARGO_TARGET_TMPDIR"), + dir.file_name().unwrap().to_str().unwrap(), + ) + .unwrap(); + create_dir_all(target_dir.path()).unwrap(); + + { + println!("-------- baseline --------"); + let result = command(&mode, &dir, target_dir.path(), Some(features_baseline)) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(!result.status.success()); + } + + { + println!("-------- with changes --------"); + let result = command(&mode, &dir, target_dir.path(), Some(features_changes)) + .output() + .unwrap(); + println!("Stdout: {}", String::from_utf8(result.stdout).unwrap()); + println!("Stderr: {}", String::from_utf8(result.stderr).unwrap()); + assert!(result.status.success()); + } +}