Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursion unwinding does not terminate #3771

Open
thanhnguyen-aws opened this issue Dec 10, 2024 · 5 comments
Open

Recursion unwinding does not terminate #3771

thanhnguyen-aws opened this issue Dec 10, 2024 · 5 comments
Labels
[C] Bug This is a bug. Something isn't working.

Comments

@thanhnguyen-aws
Copy link
Contributor

I ran a small experiment to check the equivalence of the two versions of the greatest-common-divisor function, one is written with a loop and one in recursion.

I tried this code:

fn gcd (mut u: u8, mut v: u8) -> u8 {
    if u == 0 { return v; }
    if v == 0 { return u; }
    loop {
        if u > v {
            let temp = u;
            u = v;
            v = temp;
        }
        v -= u; // here v >= u
        if v == 0 { break; }
    }
    u 
}


fn gcd_rec(x: u8, y: u8) -> u8 {
    if y == 0 {
        x
    } 
    else if x == 0 {
        y
    } 
    else if x> y {
        gcd_rec(y, x - y)
    } 
    else
    {
        gcd_rec(x, y - x)
    }
} 

#[cfg(kani)]
#[kani::proof]
#[kani::unwind(256)]
fn main() {
    let x: u8 = kani::any();
    let y: u8 = kani::any();
    assert_eq!(gcd(x,y), gcd_rec(x,y));
}

using the following command line invocation:

kani  src/main.rs

with Kani version: 0.56.0

I expected to see this happen: Kani can verify the equivalence of the two versions. Note that, for the loop version, after each iteration max(u,v) decreases at least by 1 and u, v are u8, so the maximum number of iterations (unwinding) should be 256. It is similar for the two arguments of the recursive version.

Instead, this happened: The recursive version seems to be unwinded forever.

@thanhnguyen-aws thanhnguyen-aws added the [C] Bug This is a bug. Something isn't working. label Dec 10, 2024
@feliperodri
Copy link
Contributor

feliperodri commented Dec 11, 2024

Thanks for the report, @thanhnguyen-aws! Have you tried

?

You would have to annotate the recursive function with contracts.

@zhassan-aws
Copy link
Contributor

@feliperodri The #[kani::recursion] attribute should only be needed if contracts are used. Without contracts, the unwinding should eventually terminate.

@zhassan-aws
Copy link
Contributor

Apparently, this occurs because of an exponential blow-up in the recursion tree? Here's a simpler reproducer:

fn rec(n: u8) -> u8 {
    if n == 0 {
        return 0;
    }
    //if n > 10 {
    //    return rec(n - 1);
    //}
    return rec(n - 1);
}

#[kani::proof]
#[kani::unwind(256)]
fn main() {
    let n: u8 = kani::any();
    let z = rec(n);
    assert!(z == 0);
}

This terminates immediately:

$ kani iss3771.rs
...

SUMMARY:
 ** 0 of 3 failed

VERIFICATION:- SUCCESSFUL
Verification Time: 0.57622415s

but if I enable the commented-out code (which doesn't change the semantics of the function), CBMC keeps unwinding for more than 10 minutes without terminating:

Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257
Unwinding recursion rec iteration 256
Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257
Unwinding recursion rec iteration 254
Unwinding recursion rec iteration 255
Unwinding recursion rec iteration 256
Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257
Unwinding recursion rec iteration 256
Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257
Unwinding recursion rec iteration 255
Unwinding recursion rec iteration 256
Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257
Unwinding recursion rec iteration 256
Not unwinding recursion rec iteration 257
Not unwinding recursion rec iteration 257

@tautschnig do you think there's anything that can be done here?

@zhassan-aws
Copy link
Contributor

@thanhnguyen-aws You can work around the issue by rewriting gcd_rec using tail recursion:

fn gcd_rec(x: u8, y: u8) -> u8 {
    if y == 0 {
        x
    } else if x == 0 {
        y
    } else {
        let mut x = x;
        let mut y = y;
        if y > x {
            let temp = x;
            x = y;
            y = temp;
        }
        gcd_rec(y, x - y)
    }
}

With that, verification terminates successfully in ~1 minute:

SUMMARY:
 ** 0 of 5 failed

VERIFICATION:- SUCCESSFUL
Verification Time: 56.013733s

@thanhnguyen-aws
Copy link
Contributor Author

@feliperodri @zhassan-aws Thank you very much for your response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[C] Bug This is a bug. Something isn't working.
Projects
None yet
Development

No branches or pull requests

3 participants