Skip to content

Semantics of StorageLive in loops #42371

@RalfJung

Description

@RalfJung

This code

fn factorial_loop() -> i64 {
    let mut product = 1;
    let mut i = 1;

    while i <= 10 {
        product *= i;
        i += 1;
    }

    product
}

compiles to

fn factorial_loop() -> i64 {
    let mut _0: i64;                     // return pointer
    scope 1 {
        let mut _1: i64;                 // "product" in scope 1 at src/main.rs:2:9: 2:20
        scope 2 {
            let mut _2: i64;             // "i" in scope 2 at src/main.rs:3:9: 3:14
        }
    }
    let mut _3: ();
    let mut _4: bool;
    let mut _5: i64;
    let mut _6: ();
    let mut _7: i64;
    let mut _8: (i64, bool);
    let mut _9: (i64, bool);
    let mut _10: i64;

    bb0: {
        StorageLive(_1);                 // scope 0 at src/main.rs:2:9: 2:20
        _1 = const 1i64;                 // scope 0 at src/main.rs:2:23: 2:24
        StorageLive(_2);                 // scope 1 at src/main.rs:3:9: 3:14
        _2 = const 1i64;                 // scope 1 at src/main.rs:3:17: 3:18
        goto -> bb1;                     // scope 2 at src/main.rs:5:5: 8:6
    }

    bb1: {
        StorageLive(_4);                 // scope 2 at src/main.rs:5:11: 5:18
        StorageLive(_5);                 // scope 2 at src/main.rs:5:11: 5:12
        _5 = _2;                         // scope 2 at src/main.rs:5:11: 5:12
        _4 = Le(_5, const 10i64);        // scope 2 at src/main.rs:5:11: 5:18
        StorageDead(_5);                 // scope 2 at src/main.rs:5:18: 5:18
        switchInt(_4) -> [0u8: bb2, otherwise: bb3]; // scope 2 at src/main.rs:5:5: 8:6
    }

    bb2: {
        _3 = ();                         // scope 2 at src/main.rs:5:5: 8:6
        StorageDead(_4);                 // scope 2 at src/main.rs:8:6: 8:6
        StorageLive(_10);                // scope 2 at src/main.rs:10:5: 10:12
        _10 = _1;                        // scope 2 at src/main.rs:10:5: 10:12
        _0 = _10;                        // scope 2 at src/main.rs:10:5: 10:12
        StorageDead(_10);                // scope 2 at src/main.rs:10:12: 10:12
        StorageDead(_2);                 // scope 1 at src/main.rs:11:2: 11:2
        StorageDead(_1);                 // scope 0 at src/main.rs:11:2: 11:2
        return;                          // scope 0 at src/main.rs:11:2: 11:2
    }

    bb3: {
        StorageLive(_7);                 // scope 2 at src/main.rs:6:20: 6:21
        _7 = _2;                         // scope 2 at src/main.rs:6:20: 6:21
        _8 = CheckedMul(_1, _7);         // scope 2 at src/main.rs:6:9: 6:21
        assert(!(_8.1: bool), "attempt to multiply with overflow") -> bb4; // scope 2 at src/main.rs:6:9: 6:21
    }

    bb4: {
        _1 = (_8.0: i64);                // scope 2 at src/main.rs:6:9: 6:21
        StorageDead(_7);                 // scope 2 at src/main.rs:6:21: 6:21
        _9 = CheckedAdd(_2, const 1i64); // scope 2 at src/main.rs:7:9: 7:15
        assert(!(_9.1: bool), "attempt to add with overflow") -> bb5; // scope 2 at src/main.rs:7:9: 7:15
    }

    bb5: {
        _2 = (_9.0: i64);                // scope 2 at src/main.rs:7:9: 7:15
        _6 = ();                         // scope 2 at src/main.rs:5:19: 8:6
        goto -> bb1;                     // scope 2 at src/main.rs:5:5: 8:6
    }
}

On every run through the loop, StorageLive(_4) is executed, and there is no matching StorageDead.

The intended semantics of multiple StorageLive on the same location are unclear. However, evidence suggests that LLVM considers a variable dead before any llvm.lifetime.start, in particular, in llvm.lifetime.start; load; llvm.lifetime.start, LLVM feels free to treat that load as yielding undef. The reference manual says so (however, it doesn't say anything about llvm.lifetime.start; load; llvm.lifetime.end; llvm.lifetime.start being fine, so it seems incomplete), and @eddyb managed to actually obtain UB from a redundant llvm.lifetime.start in

#![feature(link_llvm_intrinsics, untagged_unions)]

extern {
    #[link_name = "llvm.lifetime.start"]
    fn MyStorageLive(size: usize, ptr: *const u8);
}

#[allow(unions_with_drop_fields)]
union Foo<T> { x: T }

fn main() {
    unsafe {
        let mut x = Foo { x: String::new() };
        while x.x.len() < 10 {
            x.x.push('a');
            assert_eq!(std::mem::size_of_val(&x.x), 24);
            MyStorageLive(24, &x.x as *const _ as *const _);
            println!("{}", &x.x[..])
        }
    }
}

Playpen
If you comment out the MyStorageLive, this prints some a's. If you leave it in, nothing is printed.

This suggests we should rather not permit "redundant" StorageLive, and hence the program at the beginning of this report should be translated such that _4 is marked live before the loop, rather than inside the loop.

Metadata

Metadata

Assignees

Labels

A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions