Attribute for "pure inplace edit" functions

Inspired by a tangent in another thread, I think we should add an attribute to indicate that a function purely edits values in-place without other side-effects.

These two functions feel very similar to me:

pub fn foo1() {
    let mut foo = 7;
    println!("{foo}");
    foo += 1;
}

pub fn foo2() {
    let mut foo = 7;
    println!("{foo}");
    inc_one(&mut foo);
}
fn inc_one(n: &mut i32) {
    *n += 1;
}

But foo1 raises a lint for the foo += 1 assignment being unused, whereas foo2 does the same thing but raises no such lint, since rustc can't see across both functions to conclude that there's an issue.

I think it'd be useful to add an attribute #[pure_inplace_edit] (feel free to bikeshed the name) to functions to indicate that the function should be treated as an inplace edit of any &mut _ arguments for the purposes of the lint. This attribute doesn't change the behavior of the code, only whether rustc raises this lint. You could think of this attribute as the equivalent to #[must_use], but for &mut arguments instead of the return value.

So we change the above to:

#[pure_inplace_edit]
fn inc_one(n: &mut i32) {
    *n += 1;
}

And now rustc will see that inc_one(&mut foo); edits foo in-place the same as foo += 1; does, and raise the same lint.

4 Likes

Also, I forgot to mention this in my original post, but I envision it also applying to &mut self methods, so something like

impl PathBuf {
    #[pure_inplace_edit]
    pub fn push<P: AsRef<Path>>(&mut self, path: P) { .. }
}

works the same as I described normal &mut arguments on functions.

maybe a better name would be #[must_use(<the-arg-name>)], which allows you to indicate that whatever argument was passed in as <the-arg-name> should be treated as a #[must_use] output, since that describes what you want to happen, rather than the imo more confusing approach of labeling whatever purity property the function might or might not have.

that way it's also useful for things like the following, where you only want one arg to be must_use, and the function call itself isn't pure (it runs an external program):

#[must_use(stderr, "you shouldn't ignore the error output")]
/// it's ok to ignore `stdout` because ...todo...
///
/// `stdout` and `stderr` are passed as `&mut Vec<u8>` so
/// you can reuse buffers instead of allocating a new buffer each time.
pub fn run_program(args: &[impl AsRef<OsStr>], stdout: &mut Vec<u8>, stderr: &mut Vec<u8>) -> io::Result<()> {
    todo!("run the program, collecting stdout into stdout and stderr into stderr")
}
14 Likes