Skip to content

Commit f6b86ec

Browse files
authored
More reliable property attributes extraction (rust-lang#1514)
* Deserialize property IDs and rebuild them for output. * Convert property functions into methods * Put tests into tests module * Refactor format check into function * Remove redundant check * Import `super` in `tests` module * Avoid `Option<T>` and use `missing_definition` for `class` * Fix `clippy` warnings * Fix format
1 parent 1eb9643 commit f6b86ec

File tree

2 files changed

+259
-23
lines changed

2 files changed

+259
-23
lines changed

kani-driver/src/cbmc_output_parser.rs

Lines changed: 251 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use once_cell::sync::Lazy;
2626
use pathdiff::diff_paths;
2727
use regex::Regex;
2828
use rustc_demangle::demangle;
29-
use serde::Deserialize;
29+
use serde::{Deserialize, Deserializer};
3030
use std::{
3131
collections::HashMap,
3232
env,
@@ -215,14 +215,123 @@ impl ParserItem {
215215
#[derive(Clone, Debug, Deserialize)]
216216
pub struct Property {
217217
pub description: String,
218-
pub property: String,
218+
#[serde(rename = "property")]
219+
pub property_id: PropertyId,
219220
#[serde(rename = "sourceLocation")]
220221
pub source_location: SourceLocation,
221222
pub status: CheckStatus,
222223
pub reach: Option<CheckStatus>,
223224
pub trace: Option<Vec<TraceItem>>,
224225
}
225226

227+
#[derive(Clone, Debug)]
228+
pub struct PropertyId {
229+
pub fn_name: Option<String>,
230+
pub class: String,
231+
pub id: u32,
232+
}
233+
234+
impl Property {
235+
pub fn property_class(&self) -> String {
236+
self.property_id.class.clone()
237+
}
238+
239+
pub fn property_name(&self) -> Result<String> {
240+
use std::fmt::Write;
241+
let mut fmt_str = String::new();
242+
let prop_id = &self.property_id;
243+
if let Some(name) = &prop_id.fn_name {
244+
write!(&mut fmt_str, "{name}.")?;
245+
}
246+
let class = &prop_id.class;
247+
let id = prop_id.id;
248+
write!(&mut fmt_str, "{class}.")?;
249+
write!(&mut fmt_str, "{id}")?;
250+
Ok(fmt_str)
251+
}
252+
253+
pub fn has_property_class_format(string: &str) -> bool {
254+
string == "NaN" || string.chars().all(|c| c.is_ascii_lowercase() || c == '_' || c == '-')
255+
}
256+
}
257+
258+
impl<'de> serde::Deserialize<'de> for PropertyId {
259+
/// Gets all property attributes from the property ID.
260+
///
261+
/// In general, property IDs have the format `<function>.<class>.<counter>`.
262+
///
263+
/// However, there are cases where we only get two attributes:
264+
/// * `<class>.<counter>` (the function is a CBMC builtin)
265+
/// * `<function>.<counter>` (missing function definition)
266+
///
267+
/// In these cases, we try to determine if the attribute is a function or not
268+
/// based on its characters (we assume property classes are a combination
269+
/// of lowercase letters and the characters `_` and `-`). But this is not completely
270+
/// reliable. CBMC should be able to provide these attributes as separate fields
271+
/// in the JSON output: <https://github.com/diffblue/cbmc/issues/7069>
272+
fn deserialize<D>(d: D) -> Result<Self, D::Error>
273+
where
274+
D: Deserializer<'de>,
275+
{
276+
let id_str = String::deserialize(d)?;
277+
278+
// Handle a special case that doesn't respect any format. It appears at
279+
// least in the test `tests/expected/dynamic-error-trait/main.rs` and its
280+
// description is is: "recursion unwinding assertion"
281+
if id_str == ".recursion" {
282+
return Ok(PropertyId { fn_name: None, class: "recursion".to_owned(), id: 1 });
283+
};
284+
285+
// Split the property name into three from the end, using `.` as the separator
286+
let property_attributes: Vec<&str> = id_str.rsplitn(3, '.').collect();
287+
let attributes_tuple = match property_attributes.len() {
288+
// The general case, where we get all the attributes
289+
3 => {
290+
// Since mangled function names may contain `.`, we check if
291+
// `property_attributes[1]` has the class format. If it doesn't,
292+
// it means we've split a function name, so we rebuild it and
293+
// demangle it.
294+
if Property::has_property_class_format(property_attributes[1]) {
295+
let name = format!("{:#}", demangle(property_attributes[2]));
296+
(Some(name), property_attributes[1], property_attributes[0])
297+
} else {
298+
let full_name =
299+
format!("{}.{}", property_attributes[2], property_attributes[1]);
300+
let name = format!("{:#}", demangle(&full_name));
301+
(Some(name), "missing_definition", property_attributes[0])
302+
}
303+
}
304+
2 => {
305+
// The case where `property_attributes[1]` could be a function
306+
// or a class. If it has the class format, then it's likely a
307+
// class (functions are usually mangled names which contain many
308+
// other symbols).
309+
if Property::has_property_class_format(property_attributes[1]) {
310+
(None, property_attributes[1], property_attributes[0])
311+
} else {
312+
let name = format!("{:#}", demangle(property_attributes[1]));
313+
(Some(name), "missing_definition", property_attributes[0])
314+
}
315+
}
316+
// The case we don't expect. It's best to fail with an informative message.
317+
_ => unreachable!("Found property which doesn't have 2 or 3 attributes"),
318+
};
319+
// Do more sanity checks, just in case.
320+
assert!(
321+
attributes_tuple.2.chars().all(|c| c.is_ascii_digit()),
322+
"Found property counter that doesn't match number format"
323+
);
324+
// Return tuple after converting counter from string into number.
325+
// Safe to do because we've checked the format earlier.
326+
let class = String::from(attributes_tuple.1);
327+
Ok(PropertyId {
328+
fn_name: attributes_tuple.0,
329+
class,
330+
id: attributes_tuple.2.parse().unwrap(),
331+
})
332+
}
333+
}
334+
226335
/// Struct that represents a source location.
227336
///
228337
/// Source locations may be completely empty, which is why
@@ -625,7 +734,7 @@ fn format_result(properties: &Vec<Property>, show_checks: bool) -> String {
625734
}
626735

627736
for prop in properties {
628-
let name = &prop.property;
737+
let name = prop.property_name().unwrap();
629738
let status = &prop.status;
630739
let description = &prop.description;
631740
let location = &prop.source_location;
@@ -848,9 +957,9 @@ fn modify_undefined_function_checks(mut properties: Vec<Property>) -> (Vec<Prope
848957
/// temporary variable in their descriptions.
849958
fn get_readable_description(property: &Property) -> String {
850959
let original = property.description.clone();
851-
let class_id = extract_property_class(property).unwrap();
960+
let class_id = property.property_class();
852961

853-
let description_alternatives = CBMC_ALT_DESCRIPTIONS.get(class_id);
962+
let description_alternatives = CBMC_ALT_DESCRIPTIONS.get(&class_id as &str);
854963
if let Some(alt_descriptions) = description_alternatives {
855964
for (desc_to_match, opt_desc_to_replace) in alt_descriptions {
856965
if original.contains(desc_to_match) {
@@ -914,14 +1023,6 @@ fn remove_check_ids_from_description(mut properties: Vec<Property>) -> Vec<Prope
9141023
properties
9151024
}
9161025

917-
/// Extracts the property class from the property string.
918-
///
919-
/// Property strings have the format `([<function>.]<property_class_id>.<counter>)`
920-
pub fn extract_property_class(property: &Property) -> Option<&str> {
921-
let property_class: Vec<&str> = property.property.rsplitn(3, '.').collect();
922-
if property_class.len() > 1 { Some(property_class[1]) } else { None }
923-
}
924-
9251026
/// Given a description, this splits properties into two groups:
9261027
/// 1. Properties that don't contain the description
9271028
/// 2. Properties that contain the description
@@ -948,8 +1049,7 @@ fn filter_sanity_checks(properties: Vec<Property>) -> Vec<Property> {
9481049
properties
9491050
.into_iter()
9501051
.filter(|prop| {
951-
!(extract_property_class(prop).unwrap() == "sanity_check"
952-
&& prop.status == CheckStatus::Success)
1052+
!(prop.property_class() == "sanity_check" && prop.status == CheckStatus::Success)
9531053
})
9541054
.collect()
9551055
}
@@ -962,8 +1062,8 @@ fn filter_ptr_checks(properties: Vec<Property>) -> Vec<Property> {
9621062
properties
9631063
.into_iter()
9641064
.filter(|prop| {
965-
!extract_property_class(prop).unwrap().contains("pointer_arithmetic")
966-
&& !extract_property_class(prop).unwrap().contains("pointer_primitives")
1065+
!prop.property_class().contains("pointer_arithmetic")
1066+
&& !prop.property_class().contains("pointer_primitives")
9671067
})
9681068
.collect()
9691069
}
@@ -1032,3 +1132,137 @@ fn determine_verification_result(properties: &[Property]) -> bool {
10321132
properties.iter().filter(|prop| prop.status == CheckStatus::Failure).count();
10331133
number_failed_properties == 0
10341134
}
1135+
1136+
#[cfg(test)]
1137+
mod tests {
1138+
use super::*;
1139+
1140+
#[test]
1141+
fn check_property_id_deserialization_general() {
1142+
let prop_id_string = "\"alloc::raw_vec::RawVec::<u8>::allocate_in.sanity_check.1\"";
1143+
let prop_id_result: Result<PropertyId, serde_json::Error> =
1144+
serde_json::from_str(prop_id_string);
1145+
let prop_id = prop_id_result.unwrap();
1146+
assert_eq!(
1147+
prop_id.fn_name,
1148+
Some(String::from("alloc::raw_vec::RawVec::<u8>::allocate_in"))
1149+
);
1150+
assert_eq!(prop_id.class, String::from("sanity_check"));
1151+
assert_eq!(prop_id.id, 1);
1152+
1153+
let dummy_prop = Property {
1154+
description: "".to_string(),
1155+
property_id: prop_id,
1156+
source_location: SourceLocation {
1157+
function: None,
1158+
file: None,
1159+
column: None,
1160+
line: None,
1161+
},
1162+
status: CheckStatus::Success,
1163+
reach: None,
1164+
trace: None,
1165+
};
1166+
assert_eq!(
1167+
dummy_prop.property_name().unwrap(),
1168+
prop_id_string[1..prop_id_string.len() - 1]
1169+
);
1170+
}
1171+
1172+
#[test]
1173+
fn check_property_id_deserialization_only_name() {
1174+
let prop_id_string = "\"alloc::raw_vec::RawVec::<u8>::allocate_in.1\"";
1175+
let prop_id_result: Result<PropertyId, serde_json::Error> =
1176+
serde_json::from_str(prop_id_string);
1177+
dbg!(&prop_id_result);
1178+
let prop_id = prop_id_result.unwrap();
1179+
assert_eq!(
1180+
prop_id.fn_name,
1181+
Some(String::from("alloc::raw_vec::RawVec::<u8>::allocate_in"))
1182+
);
1183+
assert_eq!(prop_id.class, "missing_definition");
1184+
assert_eq!(prop_id.id, 1);
1185+
1186+
let dummy_prop = Property {
1187+
description: "".to_string(),
1188+
property_id: prop_id,
1189+
source_location: SourceLocation {
1190+
function: None,
1191+
file: None,
1192+
column: None,
1193+
line: None,
1194+
},
1195+
status: CheckStatus::Success,
1196+
reach: None,
1197+
trace: None,
1198+
};
1199+
assert_eq!(
1200+
dummy_prop.property_name().unwrap(),
1201+
"alloc::raw_vec::RawVec::<u8>::allocate_in.missing_definition.1"
1202+
);
1203+
}
1204+
1205+
#[test]
1206+
fn check_property_id_deserialization_only_class() {
1207+
let prop_id_string = "\"assertion.1\"";
1208+
let prop_id_result: Result<PropertyId, serde_json::Error> =
1209+
serde_json::from_str(prop_id_string);
1210+
let prop_id = prop_id_result.unwrap();
1211+
assert_eq!(prop_id.fn_name, None);
1212+
assert_eq!(prop_id.class, String::from("assertion"));
1213+
assert_eq!(prop_id.id, 1);
1214+
1215+
let dummy_prop = Property {
1216+
description: "".to_string(),
1217+
property_id: prop_id,
1218+
source_location: SourceLocation {
1219+
function: None,
1220+
file: None,
1221+
column: None,
1222+
line: None,
1223+
},
1224+
status: CheckStatus::Success,
1225+
reach: None,
1226+
trace: None,
1227+
};
1228+
assert_eq!(
1229+
dummy_prop.property_name().unwrap(),
1230+
prop_id_string[1..prop_id_string.len() - 1]
1231+
);
1232+
}
1233+
1234+
#[test]
1235+
fn check_property_id_deserialization_special() {
1236+
let prop_id_string = "\".recursion\"";
1237+
let prop_id_result: Result<PropertyId, serde_json::Error> =
1238+
serde_json::from_str(prop_id_string);
1239+
let prop_id = prop_id_result.unwrap();
1240+
assert_eq!(prop_id.fn_name, None);
1241+
assert_eq!(prop_id.class, String::from("recursion"));
1242+
assert_eq!(prop_id.id, 1);
1243+
1244+
let dummy_prop = Property {
1245+
description: "".to_string(),
1246+
property_id: prop_id,
1247+
source_location: SourceLocation {
1248+
function: None,
1249+
file: None,
1250+
column: None,
1251+
line: None,
1252+
},
1253+
status: CheckStatus::Success,
1254+
reach: None,
1255+
trace: None,
1256+
};
1257+
assert_eq!(dummy_prop.property_name().unwrap(), "recursion.1");
1258+
}
1259+
1260+
#[test]
1261+
#[should_panic]
1262+
fn check_property_id_deserialization_panics() {
1263+
let prop_id_string = "\"not_a_property_ID\"";
1264+
let prop_id_result: Result<PropertyId, serde_json::Error> =
1265+
serde_json::from_str(prop_id_string);
1266+
let _prop_id = prop_id_result.unwrap();
1267+
}
1268+
}

kani-driver/src/concrete_playback.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ struct UnitTest {
312312
/// ..., ] }
313313
/// ```
314314
mod concrete_vals_extractor {
315-
use crate::cbmc_output_parser::{extract_property_class, CheckStatus, ParserItem, TraceItem};
315+
use crate::cbmc_output_parser::{CheckStatus, ParserItem, TraceItem};
316316

317317
pub struct ConcreteVal {
318318
pub byte_arr: Vec<u8>,
@@ -331,24 +331,26 @@ mod concrete_vals_extractor {
331331
.expect("Missing CBMC result.");
332332

333333
let mut failures = result_item.iter().filter(|prop| {
334-
extract_property_class(prop).expect("Unexpected property class.") == "assertion"
335-
&& prop.status == CheckStatus::Failure
334+
prop.property_class() == "assertion" && prop.status == CheckStatus::Failure
336335
});
337336

338337
// Process the first assertion failure.
339338
let first_failure = failures.next();
340339
if let Some(property) = first_failure {
341340
// Extract values for the first assertion that has failed.
342-
let trace =
343-
property.trace.as_ref().expect(&format!("Missing trace for {}", property.property));
341+
let trace = property
342+
.trace
343+
.as_ref()
344+
.expect(&format!("Missing trace for {}", property.property_name().unwrap()));
344345
let concrete_vals = trace.iter().filter_map(&extract_from_trace_item).collect();
345346

346347
// Print warnings for all the other failures that were not handled in case they expected
347348
// even future checks to be extracted.
348349
for unhandled in failures {
349350
println!(
350351
"WARNING: Unable to extract concrete values from multiple failing assertions. Skipping property `{}` with description `{}`.",
351-
unhandled.property, unhandled.description,
352+
unhandled.property_name().unwrap(),
353+
unhandled.description,
352354
);
353355
}
354356
Some(concrete_vals)

0 commit comments

Comments
 (0)