The i32 field is Copy, so the async move block is mutating a copy. If you replace the inner type with one that is not Copy, you will get a partial move error:
As expected, you can replace the async move block with a move closure that has the same effect.
fn main() {
let mut m = Value(1);
(move || {
m.0 = 123;
})();
println!("m: {m:?}");
}
struct Value(i32) is a simple tuple struct wrapping an i32.
By default, types in Rust are not Copy unless explicitly derived with #[derive(Copy, Clone)] or they consist solely of Copy fields and opt into Copy. Here, Value does not derive Copy, so it is indeed a non-Copy type, meaning it follows move semantics rather than copy semantics.
I am confused that the code defines Value(i32) without Copy, so it should be non-Copy. Rust compiler should not treat it as Copy type implicitly.
Also by Ownership Rules, with async move, the code should fail to compile. However, the compile result is no error, and the output is m: Value(1).
The Value struct itself is indeed not Copy. However, the disjoint capture section of the Edition Guide (and linked RFC) explains that the closure does not capture the whole Value struct. It only capture the fields that it uses. In this case, the field is Copy and the closure is move.
If you compile with Edition 2018 or earlier, it will fail. The mental model you describe fits the implementation in these older editions, before disjoint capture in closures was added.