Ownership and moving in match statement with and without `return` keyword

I am writing a function to perform semantic analysis over a simple assembly language instruction I am implementing for a simple computer model. I use some match expressions to acheive this, and upon detecting errors I return a custom error type. The error type takes owned data, so the algorithm conditionally moves values into the error.

I found that on the branches where I construct this error, if I use the return keyword the compiler understands this conditional move ends the function, and lets me use the variables after the match, but if I wait until the end of the expression and use ?, it considers the variable moved and I cannot use them later. Here's some code illustrating this:

// ensure the destination is not Immediate or a condition bit
            match &dest {
                // construct error, but do not use `return`
                Operand::Immediate(_) | Operand::ConditionBit(_) => Err(
                    OsiacSemanticError::IllegalDestinationAddressingMode(dest, op.to_string()),
                ),
                _ => Ok(()),
            }?;

            return Ok(Instruction::DoubleOperand { op, src, dest });

This code results in this Rust compiler error message:

error[E0382]: use of moved value: `dest`
// ensure the destination is not Immediate or a condition bit
            match &dest {
                // construct and directly return error
                Operand::Immediate(_) | Operand::ConditionBit(_) => return Err(
                    OsiacSemanticError::IllegalDestinationAddressingMode(dest, op.to_string()),
                ),
                _ => Ok(()),
            }?;

            return Ok(Instruction::DoubleOperand { op, src, dest });

This results in no compiler error.

I want to understand better why this is the case. I suspect because without the return statement, the match statement scope ends before the ? operator is used, meaning it compiles as a scope where the value might move, therefore the compiler treats it as moved. With the return statement my guess is that the compiler recognizes the immediately returning branch and that using the value after that must be safe as if it moved execution would not reach there anyway.

Sorry for rambling, just here to learn. Thanks all!

1 Like

When you use the return keyword, the compiler understands that the function ends there. I'm also intrigued about why the try operator ? is not treated as an early return :thinking:; my understanding is that it compiled down to equivalent code.

1 Like

I think conceptually it's the same reason why this code won't compile:

fn foo(s: String, b: bool) -> String {
    // the match part
    let o = if b {
        Some(s) 
    } else {
        None
    };
    
    // the `?` part
    if let Some(o) = o {
        return o;
    } else {
        return s;
    }
}
2 Likes

Or why like these don't compile:

fn foo(s: String, bad: bool) -> Result<String, ()> {
    match {
        match () {
            () if bad => {
                let _move_s = s;
                false
            }
            () => true,
        }
    } {
        true => (),
        false => return Err(()),
    }

    Ok(s)
}

fn bar(s: String) -> String {
    if false {
        let _move_s = s;
    }

    s
}

Values don't impact control flow analysis, even if they are literal -- much less constructed and then matched. And these non-compiling examples are simpler than ? in other ways, as that is implemented via trait methods. The language is not willing to use the construction of the Err(_) by moving dest and subsequent ? as proof that the following code relying on dest will not execute.

When you return immediately after moving dest, on the other hand, the compiler can tell / is willing to exploit the fact that no more code in the function executes after the move + return.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.