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

Implement support for generators #1378

Merged
merged 21 commits into from
Jul 25, 2022
Merged

Conversation

fzaiser
Copy link
Contributor

@fzaiser fzaiser commented Jul 14, 2022

Description of changes:

This implements the generator feature for Rust. We translate a generator type similarly to an enum with a variant for each suspend point.

Consider the following generator:

|| {
    let a = true;
    let b = &a;
    yield;
    assert_eq!(b as *const _, &a as *const _);
    yield;
};

Rustc compiles this to something similar to the following enum (but there are differences, see below!):

enum GeneratorEnum {
    Unresumed,                        // initial state of the generator
    Returned,                         // generator has returned
    Panicked,                         // generator has panicked
    Suspend0 { b: &bool, a: bool },   // state after suspending (`yield`ing) for the first time
    Suspend1,                         // state after suspending (`yield`ing) for the second time
}

However, its layout may differ from normal Rust enums in the following ways:

  • Contrary to enums, the discriminant may not be at offset 0.
  • Contrary to enums, there may be other fields than the discriminant "at the top level" (outside the variants).

This means that we CANNOT use the enum translation, which would be roughly as follows:

struct GeneratorEnum {
    int case; // discriminant
    union GeneratorEnum-union cases; // variant
}

union GeneratorEnum-union {
    struct Unresumed variant0;
    struct Returned variant1;
    // ...
}

Instead, we use the following translation:

union GeneratorEnum {
    struct DirectFields direct_fields;
    struct Unresumed generator_variant_Unresumed;
    struct Returned generator_variant_Returned;
    // ...
}

struct DirectFields {
    // padding (for bool *b in Suspend0 below)
    char case;
    // padding (for bool a in Suspend0 below)
}

struct Unresumed {
    // padding (this variant has no fields)
}

// ...

struct Suspend0 {
    bool *generator_field_0; // variable b in the generator code above
    // padding (for char case in DirectFields)
    bool generator_field_1; // variable a in the generator code above
}

Of course, if the generator has any other top-level/direct fields, they'd be included in the DirectFields struct as well.

Resolved issues:

Resolves #416

Call-outs:

  • The function ignore_var_ty became redundant, so I removed it.
  • There is some code duplication with codegen_enum. I'm not sure how to remove it because codegen_enum relies on an AdtDef, which is not available for generators (because the relevant enum is generated in MIR).
  • I added a couple of assertions in codegen_enum that document assumptions that we currently make about enum layouts. (I don't know if they're guaranteed for enums, but they don't hold for generators, so I thought it would be good to be explicit about it.)
  • One of the new tests (discriminant.rs, taken from rustc) takes a bit over a minute on my laptop. Is that too much?
    • Discussed with @danielsn, decision: leave it in, but move slow tests to a separate test suite in a follow-up PR

Testing:

  • How is this change tested? 3 existing regression tests (that now work) + 16 test cases from rustc (src/test/ui/generator/)

  • Is this a refactor change? No

Checklist

  • Each commit message has a non-empty body, explaining why the change was made
  • Methods or procedures are documented
  • Regression or unit tests are included, or existing tests cover the modified code
  • My PR is restricted to a single feature or bugfix

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add comments to your code and maybe a high level explanation of how you modeled generators in the PR body?

@fzaiser
Copy link
Contributor Author

fzaiser commented Jul 18, 2022

@celinval I added both a comment in the code and the PR.

@fzaiser fzaiser marked this pull request as ready for review July 18, 2022 22:24
@fzaiser fzaiser requested a review from a team as a code owner July 18, 2022 22:24
@@ -653,8 +651,6 @@ impl<'tcx> GotocCtx<'tcx> {
let layout = self.layout_of(dst_mir_ty);
if layout.is_zst() || dst_type.sizeof_in_bits(&self.symbol_table) == 0 {
// We ignore assignment for all zero size types
// Ignore generators too for now:
// https://github.com/model-checking/kani/issues/416
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this comment was outdated because the if condition only checks for ZST.

@celinval celinval dismissed their stale review July 18, 2022 23:59

I'll try to take a look at this tomorrow. Remove my change request since you have added a great description to this issue. Thanks!

@fzaiser fzaiser self-assigned this Jul 19, 2022
Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my first pass. I'll keep taking a look at it tomorrow. Thanks!

// as long as the path is not dynamically used. Full Generator support
// tracked in: https://github.com/model-checking/kani/issues/416

// when the path is not dynamically used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here. Can you please make this test more meaningful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test is really necessary anymore, but I'm hesitant to remove tests. It used to test that a file containing generators worked as long as generators are not actually used in the execution of the program. Should I remove it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either remove it or add a check at the end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the test completely. It now checks whether generators work with resume types that are not (). I don't think the original test makes sense anymore, now that generators are properly implemented.

tests/kani/Generator/rustc-generator-tests/discriminant.rs Outdated Show resolved Hide resolved
pub fn fn_sig_of_instance(&self, instance: Instance<'tcx>) -> Option<ty::PolyFnSig<'tcx>> {
// Adapted from `fn_sig_for_fn_abi` in compiler/rustc_middle/src/ty/layout.rs
// Code duplication tracked here: https://github.com/model-checking/kani/issues/1365
fn generator_sig(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please document what you are doing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I don't really understand what's happening there (or in closure_sig, which also has no explanatory comment). Both are copied from compiler/rustc_middle/src/ty/layout.rs. That's why I added the tracking issue: #1365

kani-compiler/src/codegen_cprover_gotoc/codegen/typ.rs Outdated Show resolved Hide resolved
kani-compiler/src/codegen_cprover_gotoc/codegen/typ.rs Outdated Show resolved Hide resolved
ty::Generator(..) => {
(self.generator_variant_name(idx), TypeOrVariant::GeneratorVariant(idx))
}
_ => unreachable!("it's a bug to reach here! {:?}", &t.kind()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ error message could be more informative

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved

let layout = self.layout_of(t);
let expr = match &layout.variants {
Variants::Single { .. } => {
assert!(!t.is_generator());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be clearer to lift these to a precondition

if t.is_generator {
    assert!(matches!(layout.variants, Variants::Single { .. });
    etc ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a function check_generator_layout_assumptions because this code was repeated in a few places.

kani-compiler/src/codegen_cprover_gotoc/codegen/rvalue.rs Outdated Show resolved Hide resolved
kani-compiler/src/codegen_cprover_gotoc/codegen/typ.rs Outdated Show resolved Hide resolved
kani-compiler/src/codegen_cprover_gotoc/codegen/typ.rs Outdated Show resolved Hide resolved
// Contrary to generators, currently enums have only one field (the discriminant), the rest are in the variants:
assert!(layout.fields.count() <= 1);
// Contrary to generators, the discriminant is the first (and only) field for enums:
assert_eq!(*tag_field, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in a Niche encoding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that they have a tag_field anyway: https://doc.rust-lang.org/stable/nightly-rustc/rustc_target/abi/enum.Variants.html#variant.Multiple.field.tag_field. So this assertion should not be a problem (and the regression tests pass).

@fzaiser
Copy link
Contributor Author

fzaiser commented Jul 20, 2022

Regarding the assertions checking that the layout of a generator is multiple variants, with direct encoding: I created a function check_generator_layout_assumptions because there were many places where you suggested we check these assumptions. But thinking about it more, I think it's enough to check it in once, when the generator is created, i.e. in codegen_ty_generator. The later checks seem redundant. Do you agree and can I remove them?

Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the issue #1395, can you please inject a unimplemented failure if the user tried to get the size of a generator? Silently failing might result in an unsound analysis.

(self.generator_variant_name(idx), TypeOrVariant::GeneratorVariant(idx))
}
_ => unreachable!(
"cannot downcast {:?} to a varian (only enums and generators can)",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a much nicer error message. :)

@fzaiser
Copy link
Contributor Author

fzaiser commented Jul 25, 2022

Regarding the issue #1395, can you please inject a unimplemented failure if the user tried to get the size of a generator? Silently failing might result in an unsound analysis.

@celinval I first thought this was a good idea and I just tried it, but it turns out that 5 of the other tests rely on size_of, so we'd have to deactivate those as well, which I think does more harm than good. The WASM backend has similarly decided that it's fine to disable that one test where the size differs. (Why LLVM computes a different size is still a mystery to me. All the code I can find in rustc's codegen uses layout.size.bytes(), which is exactly what we're doing.)

@fzaiser
Copy link
Contributor Author

fzaiser commented Jul 25, 2022

Regarding the assertions checking that the layout of a generator is multiple variants, with direct encoding: I created a function check_generator_layout_assumptions because there were many places where you suggested we check these assumptions. But thinking about it more, I think it's enough to check it in once, when the generator is created, i.e. in codegen_ty_generator. The later checks seem redundant. Do you agree and can I remove them?

I have decided to remove the additional checks and only check the assumption once, when we create the generator. This is what we do for other assumptions as well: check them on creation, not every time the datatype is used.

Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this!

take(x);
};

// FIXME: for some reason, these asserts are very hard for CBMC to figure out
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please create an issue to take a look at the performance of this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #1406

@fzaiser fzaiser merged commit c4ddf6c into model-checking:main Jul 25, 2022
@fzaiser fzaiser mentioned this pull request Jul 27, 2022
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Support Rust's Generator feature
3 participants