-
Notifications
You must be signed in to change notification settings - Fork 92
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
Unwind Attribute without Python Changes #846
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unwinding values should be part of the harness metadata and not a separate struct. For me it is okay to keep it a struct (not just a u32
value) since long term we want to support unwinding values for individual loops in addition to this one.
Updated the PR as per suggestions. |
|
||
// In the case when there's only one proof attribute (correct behavior), create harness and modify it | ||
// depending on each subsequent attribute that's being called by the user. | ||
if proof_attribute_vector.len() == 1 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These cases do not seem to overlap so I am not sure why you are using if-then-else statements here. I would also recommend handling them in small functions if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They do overlap as it's necessary to check to how many proof attributes have been declared per harness and depending on the number, there's different handling that needs to happen. The entire function has a single purpose so it didn't seem necessary to factor it out into smaller functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jaisnan, if I understand this code correctly, you are trying to restrict any other kani
attribute to things that are tagged with the proof
attribute, right?
If that is the case, I would recommend to change this to have something like the following structure:
let attrs: kani_attributes(self.tcx.get_attrs(instance.def_id())); // BTreeMap<&str, &rustc_ast::Attribute>
if attrs.contains_key("proof") {
// Handle attributes here.
} else {
if attrs.len() > 0 {
// Warning: We currently only support attributes on proof harnesses. The attribute {} will be ignored.
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I am trying to restrict it to kani attribute but also use count information for each of the attributes.
I think using a BTreeMap would prevent us from checking if there are duplicate attributes in the same harness or not. If someone calls two unwind
annotations then, the last one is going to be read and parsed if we use a BTreeMap I think.
We ideally should not just check if a key exists but also how many instances of them. Maybe I can use a multimap? https://docs.rs/btreemultimap/latest/btreemultimap/ ? I think using two vectors and a match statement serves these purposes well enough. If we add more options as attributes, we'd just have to expand the match statement and add a handler and that's it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's talk offline about this one. There are other things we can go to simplify the code.
@@ -17,6 +17,8 @@ pub struct HarnessMetadata { | |||
pub original_file: String, | |||
/// The line in that file where the proof harness begins | |||
pub original_line: String, | |||
/// Optional data to store unwind value | |||
pub unwind_value: Option<u128>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we agreed having a specific struct for unwind data where this would be an optional value. Also, Option<u32>
should be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A struct didn't seem necessary for now as we're just targeting a default value for the entire harness. The struct and related changes will be added with the changes to map each loop to it's own harness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe create a type alias here that we can extend later.
type UnwindMetadata = usize;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we wantusize
or u32/64
. Doesnt usize
tie the limit of the unwind value to the target architecture?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggested usize
because that's usually used to restrict structure sizes, but in practice, I highly doubt someone is going to use a large unwind parameter. So u32
is fine too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO stay an integer. Any future unwinding information (e.g. unwindset) can be provided in separate fields. We're never going to change unwind_value
because it's just the thing we pass to cbmc as --unwind
and that's it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CBMC supports min and max unwind notation as well. But I do agree that if we are going to add a type, we should also change this to unwind
.
Since min / max is an obvious extension here, maybe we should just add the struct and encode this as a map right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why any future support for those wouldn't just add an unwind_max_value
as well. There doesn't seem to be a value to nesting it. We control the producer (kani-compiler) and consumer (cargo-kani) of this structure, so if it's looks like a mistake later, we can just fix it. So simplest choice seems best to me.
But I don't feel strongly about it.
} | ||
} | ||
|
||
// #[kani::proof] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this commented code included? If there is a reason to, add a comment explaining why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change these to be UI tests that we ensure that Kani prints the correct error message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
…to Unwind-Attribute
There was a problem hiding this 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! I have a few suggestions to clean the code a bit. Let me know if you disagree with any of them. :)
format!("Please use '#kani[proof]' above the annotation {}", attribute_vector[0].0) | ||
.as_str(), | ||
); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When do we expect to hit this else? Should we trigger an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When neither proof or any other attributes are used. This function handle_kanitool_attributes
is called on all instances in codegen_function
so raising an error might break a lot of cases. Hence, the empty handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, either remove the else or add a comment to explain what cases it is covering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
|
||
// In the case when there's only one proof attribute (correct behavior), create harness and modify it | ||
// depending on each subsequent attribute that's being called by the user. | ||
if proof_attribute_vector.len() == 1 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jaisnan, if I understand this code correctly, you are trying to restrict any other kani
attribute to things that are tagged with the proof
attribute, right?
If that is the case, I would recommend to change this to have something like the following structure:
let attrs: kani_attributes(self.tcx.get_attrs(instance.def_id())); // BTreeMap<&str, &rustc_ast::Attribute>
if attrs.contains_key("proof") {
// Handle attributes here.
} else {
if attrs.len() > 0 {
// Warning: We currently only support attributes on proof harnesses. The attribute {} will be ignored.
}
}
} | ||
} | ||
|
||
// #[kani::proof] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change these to be UI tests that we ensure that Kani prints the correct error message?
} else { | ||
// User warning for when there's more than one unwind attribute, in this case, only the first value will be | ||
self.tcx.sess.span_err(attr.span, "Use only one Unwind Annotation per Harness"); | ||
} | ||
} | ||
} | ||
|
||
/// If the attribute is named `kanitool::name`, this extracts `name` | ||
fn kanitool_attr_name(attr: &ast::Attribute) -> Option<String> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this should return Option<&str>
since we keep converting it to &str
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning Option<&str> would return a dangling pointer as the original string would be destroyed once the function returns. We need to return an Owned String so I don't think we can replace the return type.
Reference - https://stackoverflow.com/questions/43079077/proper-way-to-return-a-new-string-in-rust
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I didn't notice that you changed the code to join the segments. For the original code, the return value would have the same lifetime as the argument attr
.
BTW, why did you change it? This code might actually be doing the wrong thing. I.e., let's say you have the following attribute:
#[kanitool::type::sub_type]
I believe that you are going to return the string typesub_type
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe so, but I don't see the need to modify it to potential future attributes when there's no clear consensus on what attributes we're planning on supporting. Imo we can always change this later to adapt to future annotations.
Let me check about the scenario you've posted however.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talked offline, this is no longer needed. Going to revert the change.
// Vectors for storing instances of each attribute type, | ||
// TODO: This can be modifed to use Enums when more options are provided | ||
let mut attribute_vector = vec![]; | ||
let mut proof_attribute_vector = vec![]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, shouldn't this be a boolean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to maintain a count of the instances apart from the attributes themselves, hence vectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some notes from our chat.
else if proof_attribute_vector.len() > 1 { | ||
self.tcx | ||
.sess | ||
.span_err(proof_attribute_vector[0].span, "Only one Proof Attribute allowed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduce this to warning.
_ => None, | ||
} | ||
} | ||
// Return none if there are no unwind attributes or if there's too many unwind attributes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unwind comment in the generic function.
} | ||
|
||
/// Unwind strings of the format 'unwind_x' and the mangled names are to be parsed for the value 'x' | ||
fn handle_kanitool_unwind(&mut self, attr: &Attribute, harness: &mut HarnessMetadata) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, not a biggie. Let's move on.
} else { | ||
// User warning for when there's more than one unwind attribute, in this case, only the first value will be | ||
self.tcx.sess.span_err(attr.span, "Use only one Unwind Annotation per Harness"); | ||
} | ||
} | ||
} | ||
|
||
/// If the attribute is named `kanitool::name`, this extracts `name` | ||
fn kanitool_attr_name(attr: &ast::Attribute) -> Option<String> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talked offline, this is no longer needed. Going to revert the change.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there!
library/kani_macros/src/lib.rs
Outdated
@@ -9,6 +9,7 @@ | |||
// RUSTFLAGS="-Zcrate-attr=feature(register_tool) -Zcrate-attr=register_tool(kanitool)" | |||
|
|||
// proc_macro::quote is nightly-only, so we'll cobble things together instead | |||
extern crate proc_macro; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was this necessary?
library/kani_macros/src/lib.rs
Outdated
// The attribute '#[kani::unwind(arg)]' can only be called alongside '#[kani::proof]'. | ||
// arg - Takes in a integer value (u32) that represents the unwind value for the harness. | ||
// Usage is restricted to proof harnesses only currently. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc-comments in Rust use 3 /
s not 2, check all your function comments for this.
let attributes_list = self.tcx.get_attrs(self.current_fn().instance().def_id()); | ||
let (proof_attribute_vector, attribute_vector) = create_attribute_vectors(attributes_list); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming suggestion: don't put the type into the name really.
IMO, these names are clearer as "all_attributes" "proof_attributes" "other_attributes".
if proof_attribute_count == 1 { | ||
self.create_proof_harness(attribute_vector); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a very common programming convention to write functions like:
fn foo() {
if error condition {
// bail, whatever
}
if other error {
// bail
}
// etc
// no errors:
the conventional, expected case.
}
So try that. Spot errors, raise them and return. Then just unconditionally create_proof_harness
at the end of the function (no need for any empty else). By following the convention, functions are usually a lot easier to read (if only because people expect it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the no-op case, can we now early return with the empty parenthesis?
/// Update `self` (the goto context) to add the current function as a listed proof harness | ||
fn handle_kanitool_proof(&mut self) { | ||
fn handle_kanitool_proof(&mut self) -> HarnessMetadata { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function doc comment needs updating. Also, we should rename this, maybe. "default_kanitool_proof" maybe?
// Check if some unwind value doesnt already exist | ||
if harness.unwind_value.is_none() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same suggestion as previous comment on handling errors, then the expected case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to wrap the behavior with a match statement.
assert!( | ||
integer_value < u32::MAX.into(), | ||
"Value above maximum permitted value - u32::MAX" | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. But we might want to span_err
instead of assert.
|
||
// Create two vectors, proof_vector and attribute_vector which seperates the list of attributes into | ||
// proof and non-proof for easier parsing | ||
fn create_attribute_vectors( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename partition_kanitool_attributes
maybe?
Second suggestion: try improving the doc-comment enough that all the comments in the body aren't necessary anymore. This function is small enough this is possible and would very likely be an improvement.
// In the case when there's only one proof attribute (correct behavior), create harness and modify it | ||
// depending on each subsequent attribute that's being called by the user. | ||
let mut harness_metadata = self.handle_kanitool_proof(); | ||
if attribute_vector.len() > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary if
"unwind" => { | ||
self.handle_kanitool_unwind(attribute_tuple.1, &mut harness_metadata) | ||
} | ||
_ => {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we could error or warn?
} | ||
|
||
#[kani::proof] | ||
#[kani::test_annotation] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: this isn't testing your code, you'd have to write kanitool::
for that
if proof_attributes.is_empty() && !other_attributes.is_empty() { | ||
self.tcx.sess.span_err( | ||
other_attributes[0].1.span, | ||
"Please use '#kani[proof]' above the annotation", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming CI passes, looks largely good. Maybe one last typo.
* Unwind Attribute without Python Changes
* Unwind Attribute without Python Changes
* Unwind Attribute without Python Changes
* Unwind Attribute without Python Changes
Description of changes:
Adds Unwind Attribute without Python Changes. The
kani-metadata.json
file now has the following structureGiven that the rust library looks like -
Update - The PR has been updated to include handling for the following scenarios, all of which produce user errors which point to suggestions on how to fix the attribute.
Resolved issues:
Resolves - partly #600 and #689.
Call-outs:
The scripting needs to parse the metadata file and apply the unwind value to the target function.
Testing:
How is this change tested? Somewhat
Is this a refactor change? No
Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.