Skip to content

sfu-rsl/cargo-minimize

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cargo-minimize fork

A fork with changes so that the tool can be used for preserving a target function or file inside of a Rust crate instead of just compiler errors

Install

Clone the repo and use the command cargo install --profile dev --path . cargo-minimize inside of the repo to install the tool locally.

Then, you can run cargo minimize inside of any other Rust repo. The tool will actively overwrite or delete contents of files; make sure you only run this tool in repositories with some version control system like Git.

Minimizing a function

As an example, suppose we have this file, module_with_functions.rs:

pub fn a_ignore_this_transitively() -> u64 {
    60
}

pub fn b_use_this_dependency() -> u64 {
    0x101
}

pub fn c_ignore_this() -> u64 {
    let x = a_ignore_this_transitively();
    x + 10
}

pub fn use_this_other_dep() -> u64 {
    50
}

pub fn use_this(y: &str) -> u64 {
    let x = 42 + b_use_this_dependency();
    if y == "abc" {
        x + use_this_other_dep()
    } else {
        x
    }
}

If we want to keep the function use_this and remove as much code as can be done by cargo minimize, put the following attributes on the function so that it looks like

#[cfg(not(KEEP_CARGO_MINIMIZE))]
#[allow(dead_code)]
pub fn use_this(y: &str) -> u64 {
    // ...
}

If this file is in a separate module, ensure the module itself also has the attributes, otherwise cargo-minimize might just delete the module statement, and the code inside module_with_functions.rs won't even be compiled if the file itself is not being used by other modules (or if the other modules that used it have been minimized / deleted already). It might even delete all mod statements and call it a day.

#[cfg(not(KEEP_CARGO_MINIMIZE))]
#[allow(dead_code)]
mod module_with_functions;
  • The #[cfg(not(KEEP_CARGO_MINIMIZE))] is just an attribute that can be done in standard Rust in every crate. It uses a configuration KEEP_CARGO_MINIMIZE that no sane crate is going to specify anyway. The point of this attribute is that cargo-minimize will see this specific attribute and know to ignore the function.

  • The #[allow(dead_code)] is just to explicitly tell the compiler to allow the function to be unused (the cargo-minimize tool has a reliance on compiler to list the unused functions that can be deleted).

Add a script to the root of the crate: run-compile.sh. This script will be run on every bisection attempt by the cargo minimize tool to ensure we only keep bisections that will still compile the code:

#!/usr/bin/env bash

# output minimize-fmt-cargo to stdout to indicate that cargo lints will be output to stdout
# this is the default
# echo "minimize-fmt-cargo"

# The script needs to emit the output here for lints
cargo build --message-format=json
if [ $? != 0 ];
then
    # echo "No reproduction"
    exit 1
else
    exit 0
fi

Then, run the following command at the root of the crate's repo:

cargo minimize --script-path ./run-compile.sh

Using a custom unit test

A unit test could also be used to ensure that the relevant functionality is still preserved in the crate after minimization. First, write a Rust test that calls the function you want to preserve (e.g. a test function with the #[test] attribute in a file called preserve_fn_test.rs)

A script to first compile the test but don't run them (so that the compiler messages can be communicated) and then run the tests after but suppress the output is run-test.sh:

#!/usr/bin/env bash

# output minimize-fmt-cargo to stdout to indicate that cargo lints will be output to stdout
# this is the default
# echo "minimize-fmt-cargo"

TEST_NAME=preserve_fn_test

# The script needs to emit the output here for lints
cargo test --message-format=json --test $TEST_NAME --no-run
if [ $? != 0 ];
then
    # echo "No reproduction"
    exit 1
fi

CARGO_TEST_OUTPUT=$(cargo test --message-format=json --test $TEST_NAME)
CARGO_ERR_CODE=$?
if [ $CARGO_ERR_CODE = 0 ];
then
    exit 0
else
    exit 1
fi

You can avoid the need to set attributes by using the --rewrite_panics option. However, note that this will make code coverage important. The test code for the target function to preserve would have to cover all the branches; otherwise, there might be some functions used in the target function that get rewritten into panics.

Minimizing a file

To minimize a crate while preserving a file, the same steps apply, except you don't need to put the attributes on the function, just the module declaration. You should put the following attribute at the very top of the file you want to preserve in order for the Rust compiler to treat it as used:

#![allow(dead_code)]

Known limitations

You need to always verify that the function you want is preserved. You should also use git to compare file contents after minimization to e.g. possibly reformat or include something that was unnecessarily deleted.

The original README for cargo-minimize talks about some limitations, e.g. traits are not really handled. This is also a broad minimization tool; it works on the syntax tree with some guidance from the compiler on which functions are considered unused.

All code is reformatted by the tool, because the genemichaels crate is used to write code from a syntax tree input. The formatting is akin to the code that a macro could output. So, a function such as this

/// This is a doc comment
pub fn use_this(y: &str) -> u64 {
    // This is a comment
    let x = 42 + b_use_this_dependency();
    if y == "abc" {
        x + use_this_other_dep()
    } else {
        x
    }
}

would be rewritten as

#[doc ="This is a doc comment"]
pub fn use_this(y: &str) -> u64 {
    let x = 42 + b_use_this_dependency();
    if y == "abc" {
        x + use_this_other_dep()
    } else {
        x
    }
}

The crate might also struggle with certain macro syntax; it might not be able to output code that can compile, as it might output incorrect Rust syntax. Manual macro expansion can be done as a workaround.

Original cargo-minimize README

Install with cargo install --git https://github.com/Nilstrieb/cargo-minimize cargo-minimize and use with cargo minimize.

For more info, see the cookbook.

Idea

When encountering problems like internal compiler errors, it's often desirable to have a minimal reproduction that can be used by the people fixing the issue. Usually, these problems are found in big codebases. Getting from a big codebase to a small (<50 LOC) reproduction is non-trivial and requires a lot of manual work. cargo-minimize assists you with doing some minimization steps that can be easily automated for you.

How to use

For minimizing an internal compiler error on a normal cargo project, cargo minimize works out of the box. There are many configuration options available though.

Usage: cargo minimize [OPTIONS] [PATH]

Arguments:
  [PATH]  The directory/file of the code to be minimized [default: src]

Options:
      --extra-args <EXTRA_ARGS>
          Additional arguments to pass to cargo/rustc, separated by whitespace
      --cargo-subcmd <CARGO_SUBCMD>
          The cargo subcommand used to find the reproduction, seperated by whitespace (for example `miri run`) [default: build]
      --cargo-subcmd-lints <CARGO_SUBCMD_LINTS>
          The cargo subcommand used to get diagnostics like the dead_code lint from the compiler, seperated by whitespace. Defaults to the value of `--cargo-subcmd`
      --no-color
          To disable colored output
      --rustc
          This option bypasses cargo and uses rustc directly. Only works when a single file is passed as an argument
      --no-verify
          Skips testing whether the regression reproduces and just does the most aggressive minimization. Mostly useful for testing and demonstration purposes
      --verify-fn <VERIFY_FN>
          A Rust closure returning a bool that checks whether a regression reproduces. Example: `--verify-fn='|output| output.contains("internal compiler error")'`
      --env <ENV>
          Additional environment variables to pass to cargo/rustc. Example: `--env NAME=VALUE --env ANOTHER_NAME=VALUE`
      --project-dir <PROJECT_DIR>
          The working directory where cargo/rustc are invoked in. By default, this is the current working directory
      --script-path <SCRIPT_PATH>
          A path to a script that is run to check whether code reproduces. When it exits with code 0, the problem reproduces. If `--script-path-lints` isn't set, this script is also run to get lints. For lints, the `MINIMIZE_LINTS` environment variable will be set to `1`. The first line of the lint stdout or stderr can be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
      --script-path-lints <SCRIPT_PATH_LINTS>
          A path to a script that is run to get lints. The first line of stdout or stderr must be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
  -h, --help
          Print help information

Note: You can safely press Ctrl-C when running cargo-minimize. It will rollback the current minimization attempt and give you the latest known-reproducing state.

What it does

cargo-minimize is currently fairly simple. It does several passes over the source code. It treats each file in isolation. First, it applies the pass to everything in the file. If that stops the reproduction, it goes down the tree, eventually trying each candidate in isolation. It then repeats the pass until no more changes are made by it.

The currently implemented passes are the following:

  • pub is replaced by pub(crate). This does not have a real minimization effect on its own.
  • Bodies are replaced by loop {}. This greatly cuts down on the amount of things and makes many functions unused
  • Unused imports are removed
  • Unused functions are removed (this relies on the first step, as pub items are not marked as dead_code by rustc)

Possible improvements:

  • Delete more kinds of unused items
  • Inline small modules
  • Deal with dependencies (there is experimental code in the repo that inlines them)
  • Somehow deal with traits
  • Integrate more fine-grained minimization tools such as DustMite or perses

Cookbook

Normal project with ICE on cargo build

cargo minimize

An environment variable is required

cargo minimize --env RUSTFLAGS=-Zpolymorphize

Operate on a single file

cargo minimize --rustc file.rs

Other cargo subcommand

cargo minimize --cargo-subcmd clippy --extra-args "-- -Dclippy::needless_mut"

Use a full script

script.sh

#!/usr/bin/env bash

# output minimize-fmt-cargo to stdout to indicate that cargo lints will be output to stdout
# this is the default
echo "minimize-fmt-cargo"

# The script needs to emit the output here for lints
cargo build --release
if [ $? = 128 ];
then
    echo "The ICE reproduces"
    exit 0
else
    echo "No reproduction"
    exit 1
fi

cargo minimize --script-path ./script.sh

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

A tool to help with minimization of Rust code

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Rust 99.3%
  • Other 0.7%