use crate::{
dataflow_domains::SetDomain,
function_target::{FunctionData, FunctionTarget},
function_target_pipeline::{FunctionTargetProcessor, FunctionTargetsHolder, FunctionVariant},
options::ProverOptions,
usage_analysis,
};
use itertools::Itertools;
use log::debug;
use move_model::{
model::{FunId, FunctionEnv, GlobalEnv, GlobalId, ModuleEnv, QualifiedId, VerificationScope},
pragmas::{
CONDITION_SUSPENDABLE_PROP, DELEGATE_INVARIANTS_TO_CALLER_PRAGMA,
DISABLE_INVARIANTS_IN_BODY_PRAGMA, VERIFY_PRAGMA,
},
};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
#[derive(Clone, Default)]
pub struct VerificationInfoV2 {
pub verified: bool,
pub inlined: bool,
}
pub fn get_info(target: &FunctionTarget<'_>) -> VerificationInfoV2 {
target
.get_annotations()
.get::<VerificationInfoV2>()
.cloned()
.unwrap_or_default()
}
pub struct InvariantAnalysisData {
pub target_fun_ids: BTreeSet<QualifiedId<FunId>>,
pub dep_fun_ids: BTreeSet<QualifiedId<FunId>>,
pub disabled_inv_fun_set: BTreeSet<QualifiedId<FunId>>,
pub non_inv_fun_set: BTreeSet<QualifiedId<FunId>>,
pub target_invariants: BTreeSet<GlobalId>,
pub funs_that_modify_inv: BTreeMap<GlobalId, BTreeSet<QualifiedId<FunId>>>,
pub invs_modified_by_fun: BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>>,
pub funs_that_modify_some_inv: BTreeSet<QualifiedId<FunId>>,
pub funs_that_delegate_to_caller: BTreeSet<QualifiedId<FunId>>,
pub friend_fun_ids: BTreeSet<QualifiedId<FunId>>,
pub disabled_invs_for_fun: BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>>,
}
fn get_target_invariants(
global_env: &GlobalEnv,
target_modules: &[ModuleEnv],
) -> BTreeSet<GlobalId> {
let target_mod_ids = target_modules
.iter()
.map(|mod_env| mod_env.get_id())
.flat_map(|target_mod_id| global_env.get_global_invariants_by_module(target_mod_id))
.collect();
target_mod_ids
}
fn compute_disabled_invs_for_fun(
global_env: &GlobalEnv,
disabled_inv_fun_set: &BTreeSet<QualifiedId<FunId>>,
invs_modified_by_fun: &BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>>,
) -> BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>> {
let mut disabled_invs_for_fun: BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>> =
BTreeMap::new();
for module_env in global_env.get_modules() {
for fun_env in module_env.get_functions() {
let fun_id = fun_env.get_qualified_id();
if disabled_inv_fun_set.contains(&fun_id) {
if let Some(modified_invs) = invs_modified_by_fun.get(&fun_id) {
let disabled_invs: BTreeSet<GlobalId> = modified_invs
.iter()
.filter(|inv_id| {
global_env
.is_property_true(
&global_env
.get_global_invariant(**inv_id)
.unwrap()
.properties,
CONDITION_SUSPENDABLE_PROP,
)
.unwrap_or(false)
})
.cloned()
.collect();
debug_print_inv_set(
global_env,
&disabled_invs,
"$$$$$$$$$$$$$$$$\ncompute_disabled_invs_for_fun",
);
disabled_invs_for_fun.insert(fun_id, disabled_invs.clone());
}
}
}
}
let mut worklist: VecDeque<QualifiedId<FunId>> = disabled_inv_fun_set.iter().cloned().collect();
while let Some(caller_fun_id) = worklist.pop_front() {
if let Some(disabled_invs_for_caller) = disabled_invs_for_fun.remove(&caller_fun_id) {
let called_funs = global_env
.get_function(caller_fun_id)
.get_called_functions();
for called_fun_id in called_funs {
let disabled_invs_for_called = disabled_invs_for_fun
.entry(called_fun_id)
.or_insert_with(BTreeSet::new);
if !disabled_invs_for_caller.is_subset(disabled_invs_for_called) {
for inv_id in &disabled_invs_for_caller {
disabled_invs_for_called.insert(*inv_id);
}
worklist.push_back(called_fun_id);
}
}
disabled_invs_for_fun.insert(caller_fun_id, disabled_invs_for_caller);
}
}
disabled_invs_for_fun
}
fn check_legal_disabled_invariants(
fun_env: &FunctionEnv,
disabled_inv_fun_set: &BTreeSet<QualifiedId<FunId>>,
non_inv_fun_set: &BTreeSet<QualifiedId<FunId>>,
funs_that_modify_some_inv: &BTreeSet<QualifiedId<FunId>>,
) {
let global_env = fun_env.module_env.env;
let fun_id = fun_env.get_qualified_id();
if non_inv_fun_set.contains(&fun_id) && funs_that_modify_some_inv.contains(&fun_id) {
if disabled_inv_fun_set.contains(&fun_id) {
global_env.error(
&fun_env.get_loc(),
"Functions must not have a disable invariant pragma when invariants are \
disabled in a transitive caller or there is a \
pragma delegate_invariants_to_caller",
);
} else if fun_env.has_unknown_callers() {
if is_fun_delegating(fun_env) {
global_env.error(
&fun_env.get_loc(),
"Public or script functions cannot delegate invariants",
)
} else {
global_env.error_with_notes(
&fun_env.get_loc(),
"Public or script functions cannot be transitively called by \
functions disabling or delegating invariants",
compute_non_inv_cause_chain(fun_env),
)
}
}
}
}
fn compute_non_inv_cause_chain(fun_env: &FunctionEnv<'_>) -> Vec<String> {
let global_env = fun_env.module_env.env;
let mut worklist: BTreeSet<Vec<QualifiedId<FunId>>> = fun_env
.get_calling_functions()
.into_iter()
.map(|id| vec![id])
.collect();
let mut done = BTreeSet::new();
let mut result = vec![];
while let Some(caller_list) = worklist.iter().next().cloned() {
worklist.remove(&caller_list);
let caller_id = *caller_list.iter().last().unwrap();
done.insert(caller_id);
let caller_env = global_env.get_function_qid(caller_id);
let display_chain = || {
vec![fun_env.get_qualified_id()]
.into_iter()
.chain(caller_list.iter().cloned())
.map(|id| global_env.get_function_qid(id).get_full_name_str())
.join(" <- ")
};
if is_fun_disabled(&caller_env) {
result.push(format!("disabled by {}", display_chain()));
} else if is_fun_delegating(&caller_env) {
result.push(format!("delegated by {}", display_chain()));
} else {
worklist.extend(
caller_env
.get_calling_functions()
.into_iter()
.filter_map(|id| {
if done.contains(&id) {
None
} else {
let mut new_caller_list = caller_list.clone();
new_caller_list.push(id);
Some(new_caller_list)
}
}),
);
}
}
if result.is_empty() {
result.push("cannot determine disabling reason (bug?)".to_owned())
}
result
}
fn compute_disabled_and_non_inv_fun_sets(
global_env: &GlobalEnv,
) -> (BTreeSet<QualifiedId<FunId>>, BTreeSet<QualifiedId<FunId>>) {
let mut non_inv_fun_set: BTreeSet<QualifiedId<FunId>> = BTreeSet::new();
let mut disabled_inv_fun_set: BTreeSet<QualifiedId<FunId>> = BTreeSet::new();
let mut worklist = vec![];
for module_env in global_env.get_modules() {
for fun_env in module_env.get_functions() {
if is_fun_disabled(&fun_env) {
let fun_id = fun_env.get_qualified_id();
disabled_inv_fun_set.insert(fun_id);
worklist.push(fun_id);
}
if is_fun_delegating(&fun_env) {
let fun_id = fun_env.get_qualified_id();
if non_inv_fun_set.insert(fun_id) {
worklist.push(fun_id);
}
}
while let Some(called_fun_id) = worklist.pop() {
let called_funs = global_env
.get_function(called_fun_id)
.get_called_functions();
for called_fun_id in called_funs {
if non_inv_fun_set.insert(called_fun_id) {
worklist.push(called_fun_id);
}
}
}
}
}
(disabled_inv_fun_set, non_inv_fun_set)
}
fn is_fun_disabled(fun_env: &FunctionEnv<'_>) -> bool {
fun_env.is_pragma_true(DISABLE_INVARIANTS_IN_BODY_PRAGMA, || false)
}
fn is_fun_delegating(fun_env: &FunctionEnv<'_>) -> bool {
fun_env.is_pragma_true(DELEGATE_INVARIANTS_TO_CALLER_PRAGMA, || false)
}
fn compute_dep_fun_ids(
global_env: &GlobalEnv,
target_modules: &[ModuleEnv],
) -> BTreeSet<QualifiedId<FunId>> {
let mut dep_fun_ids = BTreeSet::new();
for module_env in global_env.get_modules() {
for target_env in target_modules {
if target_env.is_transitive_dependency(module_env.get_id()) {
for fun_env in module_env.get_functions() {
dep_fun_ids.insert(fun_env.get_qualified_id());
}
}
}
}
dep_fun_ids
}
fn compute_funs_that_modify_inv(
global_env: &GlobalEnv,
target_invariants: &BTreeSet<GlobalId>,
targets: &mut FunctionTargetsHolder,
variant: FunctionVariant,
) -> (
BTreeMap<GlobalId, BTreeSet<QualifiedId<FunId>>>,
BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>>,
BTreeSet<QualifiedId<FunId>>,
) {
let mut funs_that_modify_inv: BTreeMap<GlobalId, BTreeSet<QualifiedId<FunId>>> =
BTreeMap::new();
let mut funs_that_modify_some_inv: BTreeSet<QualifiedId<FunId>> = BTreeSet::new();
let mut invs_modified_by_fun: BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>> =
BTreeMap::new();
for inv_id in target_invariants {
let inv_mem_use: SetDomain<_> = global_env
.get_global_invariant(*inv_id)
.unwrap()
.mem_usage
.iter()
.cloned()
.collect();
let mut fun_id_set: BTreeSet<QualifiedId<FunId>> = BTreeSet::new();
for module_env in global_env.get_modules() {
for fun_env in module_env.get_functions() {
let fun_target = targets.get_target(&fun_env, &variant);
let modified_memory = &usage_analysis::get_memory_usage(&fun_target).modified.all;
if !modified_memory.is_disjoint(&inv_mem_use) {
let fun_id = fun_env.get_qualified_id();
fun_id_set.insert(fun_id);
funs_that_modify_some_inv.insert(fun_id);
let inv_set = invs_modified_by_fun
.entry(fun_id)
.or_insert_with(BTreeSet::new);
inv_set.insert(*inv_id);
}
}
}
if !fun_id_set.is_empty() {
funs_that_modify_inv.insert(*inv_id, fun_id_set);
}
}
(
funs_that_modify_inv,
invs_modified_by_fun,
funs_that_modify_some_inv,
)
}
fn compute_friend_fun_ids(
global_env: &GlobalEnv,
target_fun_ids: &BTreeSet<QualifiedId<FunId>>,
dep_fun_ids: &BTreeSet<QualifiedId<FunId>>,
funs_that_delegate_to_caller: &BTreeSet<QualifiedId<FunId>>,
) -> BTreeSet<QualifiedId<FunId>> {
let mut friend_fun_set: BTreeSet<QualifiedId<FunId>> = BTreeSet::new();
let mut worklist: Vec<QualifiedId<FunId>> = target_fun_ids.iter().cloned().collect();
worklist.extend(dep_fun_ids.iter().cloned());
while let Some(fun_id) = worklist.pop() {
let fun_env = global_env.get_function(fun_id);
let friend_env = fun_env.get_transitive_friend();
let friend_id = friend_env.get_qualified_id();
if friend_id != fun_env.get_qualified_id() && friend_fun_set.insert(friend_id) {
worklist.push(friend_id);
}
if funs_that_delegate_to_caller.contains(&fun_id) {
let callers = fun_env.get_calling_functions();
for caller_fun_id in callers {
if !target_fun_ids.contains(&caller_fun_id)
&& !dep_fun_ids.contains(&caller_fun_id)
&& friend_fun_set.insert(caller_fun_id)
{
worklist.push(caller_fun_id);
}
}
}
}
friend_fun_set
}
#[allow(dead_code)]
fn debug_print_global_ids(global_env: &GlobalEnv, global_ids: &BTreeSet<GlobalId>) {
for inv_id in global_ids {
debug_print_inv_full(global_env, inv_id);
}
}
#[allow(dead_code)]
fn debug_print_fun_id_set(
global_env: &GlobalEnv,
fun_ids: &BTreeSet<QualifiedId<FunId>>,
set_name: &str,
) {
debug!(
"****************\n{}: {:?}",
set_name,
fun_ids
.iter()
.map(|fun_id| global_env.get_function(*fun_id).get_name_string())
.collect::<Vec<_>>()
);
}
#[allow(dead_code)]
pub fn debug_print_inv_set(
global_env: &GlobalEnv,
global_ids: &BTreeSet<GlobalId>,
set_name: &str,
) {
if global_ids.is_empty() {
return;
}
debug!("{}:", set_name);
debug!("++++++++++++++++\n{}:", set_name);
for inv_id in global_ids {
debug_print_inv_full(global_env, inv_id);
}
}
#[allow(dead_code)]
fn debug_print_inv_full(global_env: &GlobalEnv, inv_id: &GlobalId) {
let inv = global_env.get_global_invariant(*inv_id);
let loc = &inv.unwrap().loc;
debug!(
"{:?} {:?}: {}",
*inv_id,
inv.unwrap().kind,
global_env.get_source(loc).unwrap(),
);
}
#[allow(dead_code)]
fn debug_print_fun_inv_map(
global_env: &GlobalEnv,
fun_inv_map: &BTreeMap<QualifiedId<FunId>, BTreeSet<GlobalId>>,
map_name: &str,
) {
debug!("****************\nMAP NAME {}:", map_name);
for (fun_id, inv_id_set) in fun_inv_map.iter() {
let fname = global_env.get_function(*fun_id).get_name_string();
debug!("FUNCTION {}:", fname);
for inv_id in inv_id_set {
debug_print_inv_full(global_env, inv_id);
}
}
}
#[allow(dead_code)]
fn debug_print_invariant_analysis_data(
global_env: &GlobalEnv,
inv_ana_data: &InvariantAnalysisData,
) {
debug_print_fun_id_set(global_env, &inv_ana_data.target_fun_ids, "target_fun_ids");
debug_print_fun_id_set(global_env, &inv_ana_data.dep_fun_ids, "dep_fun_ids");
debug_print_fun_id_set(
global_env,
&inv_ana_data.disabled_inv_fun_set,
"disabled_inv_fun_set",
);
debug_print_fun_id_set(global_env, &inv_ana_data.non_inv_fun_set, "non_inv_fun_set");
debug_print_inv_set(
global_env,
&inv_ana_data.target_invariants,
"target_invariants",
);
debug_print_fun_inv_map(
global_env,
&inv_ana_data.invs_modified_by_fun,
"invs_modified_by_fun",
);
debug_print_fun_id_set(
global_env,
&inv_ana_data.funs_that_modify_some_inv,
"funs_that_modify_some_inv",
);
debug_print_fun_id_set(
global_env,
&inv_ana_data.funs_that_delegate_to_caller,
"funs_that_delegate_to_caller",
);
debug_print_fun_id_set(global_env, &inv_ana_data.friend_fun_ids, "friend_fun_ids");
debug_print_fun_inv_map(
global_env,
&inv_ana_data.disabled_invs_for_fun,
"disabled_invs_for_fun",
);
}
pub struct VerificationAnalysisProcessorV2();
impl VerificationAnalysisProcessorV2 {
pub fn new() -> Box<Self> {
Box::new(Self())
}
}
impl FunctionTargetProcessor for VerificationAnalysisProcessorV2 {
fn process(
&self,
targets: &mut FunctionTargetsHolder,
fun_env: &FunctionEnv<'_>,
data: FunctionData,
) -> FunctionData {
let global_env = fun_env.module_env.env;
let fun_id = fun_env.get_qualified_id();
let variant = data.variant.clone();
targets.insert_target_data(&fun_id, variant.clone(), data);
let inv_ana_data = global_env.get_extension::<InvariantAnalysisData>().unwrap();
let target_fun_ids = &inv_ana_data.target_fun_ids;
let dep_fun_ids = &inv_ana_data.dep_fun_ids;
let friend_fun_ids = &inv_ana_data.friend_fun_ids;
let funs_that_modify_some_inv = &inv_ana_data.funs_that_modify_some_inv;
if fun_env.is_pragma_true(VERIFY_PRAGMA, || true) {
let is_in_target_mod = target_fun_ids.contains(&fun_id);
let is_in_deps_and_modifies_inv =
dep_fun_ids.contains(&fun_id) && funs_that_modify_some_inv.contains(&fun_id);
let is_in_friends = friend_fun_ids.contains(&fun_id);
let is_normally_verified =
is_in_target_mod || is_in_deps_and_modifies_inv || is_in_friends;
let options = ProverOptions::get(global_env);
let is_verified = match &options.verify_scope {
VerificationScope::Public => {
(is_in_target_mod && fun_env.is_exposed())
|| is_in_deps_and_modifies_inv
|| is_in_friends
}
VerificationScope::All => is_normally_verified,
VerificationScope::Only(function_name) => {
fun_env.matches_name(function_name) && is_in_target_mod
}
VerificationScope::OnlyModule(module_name) => {
is_in_target_mod && fun_env.module_env.matches_name(module_name)
}
VerificationScope::None => false,
};
if is_verified {
debug!("marking `{}` to be verified", fun_env.get_full_name_str());
mark_verified(fun_env, variant.clone(), targets);
}
}
targets.remove_target_data(&fun_id, &variant)
}
fn name(&self) -> String {
"verification_analysis_v2".to_string()
}
fn initialize(&self, global_env: &GlobalEnv, targets: &mut FunctionTargetsHolder) {
let options = ProverOptions::get(global_env);
match &options.verify_scope {
VerificationScope::Only(name) | VerificationScope::OnlyModule(name) => {
let for_module = matches!(&options.verify_scope, VerificationScope::OnlyModule(_));
let mut target_exists = false;
for module in global_env.get_modules() {
if module.is_target() {
if for_module {
target_exists = module.matches_name(name)
} else {
target_exists = module.get_functions().any(|f| f.matches_name(name));
}
if target_exists {
break;
}
}
}
if !target_exists {
global_env.error(
&global_env.unknown_loc(),
&format!(
"{} target {} does not exist in target modules",
if for_module { "module" } else { "function" },
name
),
)
}
}
_ => {}
}
let target_modules = global_env.get_target_modules();
let target_fun_ids: BTreeSet<QualifiedId<FunId>> = target_modules
.iter()
.flat_map(|mod_env| mod_env.get_functions())
.map(|fun| fun.get_qualified_id())
.collect();
let dep_fun_ids = compute_dep_fun_ids(global_env, &target_modules);
let (disabled_inv_fun_set, non_inv_fun_set) =
compute_disabled_and_non_inv_fun_sets(global_env);
let target_invariants = get_target_invariants(global_env, &target_modules);
let (funs_that_modify_inv, invs_modified_by_fun, funs_that_modify_some_inv) =
compute_funs_that_modify_inv(
global_env,
&target_invariants,
targets,
FunctionVariant::Baseline,
);
let funs_that_delegate_to_caller = non_inv_fun_set
.intersection(&funs_that_modify_some_inv)
.cloned()
.collect();
let friend_fun_ids = compute_friend_fun_ids(
global_env,
&target_fun_ids,
&dep_fun_ids,
&funs_that_delegate_to_caller,
);
let disabled_invs_for_fun =
compute_disabled_invs_for_fun(global_env, &disabled_inv_fun_set, &invs_modified_by_fun);
for module_env in global_env.get_modules() {
for fun_env in module_env.get_functions() {
check_legal_disabled_invariants(
&fun_env,
&disabled_inv_fun_set,
&non_inv_fun_set,
&funs_that_modify_some_inv,
);
}
}
let inv_ana_data = InvariantAnalysisData {
target_fun_ids,
dep_fun_ids,
disabled_inv_fun_set,
non_inv_fun_set,
target_invariants,
funs_that_modify_inv,
invs_modified_by_fun,
funs_that_modify_some_inv,
funs_that_delegate_to_caller,
friend_fun_ids,
disabled_invs_for_fun,
};
debug_print_invariant_analysis_data(global_env, &inv_ana_data);
global_env.set_extension(inv_ana_data);
}
}
fn mark_verified(
fun_env: &FunctionEnv<'_>,
variant: FunctionVariant,
targets: &mut FunctionTargetsHolder,
) {
let actual_env = fun_env.get_transitive_friend();
if actual_env.get_qualified_id() != fun_env.get_qualified_id() {
mark_inlined(fun_env, variant.clone(), targets);
}
let options = ProverOptions::get(fun_env.module_env.env);
if !actual_env.is_explicitly_not_verified(&options.verify_scope) {
let mut info = targets
.get_data_mut(&actual_env.get_qualified_id(), &variant)
.expect("function data available")
.annotations
.get_or_default_mut::<VerificationInfoV2>();
if !info.verified {
info.verified = true;
mark_callees_inlined(&actual_env, variant, targets);
}
}
}
fn mark_inlined(
fun_env: &FunctionEnv<'_>,
variant: FunctionVariant,
targets: &mut FunctionTargetsHolder,
) {
if fun_env.is_native() || fun_env.is_intrinsic() {
return;
}
debug_assert!(
targets.get_target_variants(fun_env).contains(&variant),
"`{}` has variant `{:?}`",
fun_env.get_name().display(fun_env.symbol_pool()),
variant
);
let data = targets
.get_data_mut(&fun_env.get_qualified_id(), &variant)
.expect("function data defined");
let info = data.annotations.get_or_default_mut::<VerificationInfoV2>();
if !info.inlined {
info.inlined = true;
mark_callees_inlined(fun_env, variant, targets);
}
}
fn mark_callees_inlined(
fun_env: &FunctionEnv<'_>,
variant: FunctionVariant,
targets: &mut FunctionTargetsHolder,
) {
for callee in fun_env.get_called_functions() {
let callee_env = fun_env.module_env.env.get_function(callee);
if !callee_env.is_opaque() {
mark_inlined(&callee_env, variant.clone(), targets);
}
}
}