service/constant-propagation/DefinitelyAssignedIFields.cpp (281 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "DefinitelyAssignedIFields.h"
#include "AbstractDomain.h"
#include "BaseIRAnalyzer.h"
#include "ConstantAbstractDomain.h"
#include "ConstantPropagationAnalysis.h"
#include "IRCode.h"
#include "IRInstruction.h"
#include "PatriciaTreeMapAbstractEnvironment.h"
#include "PatriciaTreeSetAbstractDomain.h"
#include "ReducedProductAbstractDomain.h"
#include "Resolver.h"
#include "StlUtil.h"
#include "Timer.h"
#include "TypeUtil.h"
#include "Walkers.h"
namespace {
using namespace ir_analyzer;
using BoolDomain = sparta::ConstantAbstractDomain<bool>;
/**
* For each register, whether it represents the `this` parameter.
**/
using ParamDomainEnvironment =
sparta::PatriciaTreeMapAbstractEnvironment<reg_t, BoolDomain>;
/**
* Set of fields that have been read even through they were not written to.
**/
using ReadUnwrittenFieldDomainEnvironment =
sparta::PatriciaTreeSetAbstractDomain<DexField*>;
/**
* Set of fields that have been written to before ever having been read. This is
* realized via the reverse adaptor, as we want to compute the intersection on
* joins.
**/
class WrittenUnreadFieldDomainEnvironment final
: public sparta::AbstractDomainReverseAdaptor<
sparta::PatriciaTreeSetAbstractDomain<DexField*>,
WrittenUnreadFieldDomainEnvironment> {
public:
using AbstractDomainReverseAdaptor::AbstractDomainReverseAdaptor;
// Some older compilers complain that the class is not default constructible.
// We intended to use the default constructors of the base class (via using
// AbstractDomainReverseAdaptor::AbstractDomainReverseAdaptor), but some
// compilers fail to catch this. So we insert a redundant '= default'.
WrittenUnreadFieldDomainEnvironment() = default;
};
// The result of analyzing a constructor tells us...
// - which fields of the constructor's declaring class were definitely assigned,
// i.e. not read before written to
// - whether the 'this' parameter escaped
struct AnalysisResult {
std::unordered_set<const DexField*> definitely_assigned_ifields;
bool may_this_have_escaped{false};
bool operator==(const AnalysisResult& other) const {
return definitely_assigned_ifields == other.definitely_assigned_ifields &&
may_this_have_escaped == other.may_this_have_escaped;
}
};
// We track...
// - for each register, whether it represents the `this` parameter
// - which fields of the constructor's declaring class might have been read even
// though they were never written to
// - which fields of the constructor's declaring class were written to before
// ever having been read
// - whether ths 'this' parameter may have escaped
class ConstructorAnalysisEnvironment final
: public sparta::ReducedProductAbstractDomain<
ConstructorAnalysisEnvironment,
ParamDomainEnvironment,
ReadUnwrittenFieldDomainEnvironment,
WrittenUnreadFieldDomainEnvironment,
BoolDomain> {
public:
using ReducedProductAbstractDomain::ReducedProductAbstractDomain;
ConstructorAnalysisEnvironment()
: ReducedProductAbstractDomain(
std::make_tuple(ParamDomainEnvironment::top(),
ReadUnwrittenFieldDomainEnvironment(),
WrittenUnreadFieldDomainEnvironment(),
BoolDomain(false))) {}
static void reduce_product(std::tuple<ParamDomainEnvironment,
ReadUnwrittenFieldDomainEnvironment,
WrittenUnreadFieldDomainEnvironment,
BoolDomain>&) {}
const ParamDomainEnvironment& get_params() const {
return ReducedProductAbstractDomain::get<0>();
}
const ReadUnwrittenFieldDomainEnvironment& get_read_unwritten_fields() const {
return ReducedProductAbstractDomain::get<1>();
}
const WrittenUnreadFieldDomainEnvironment& get_written_unread_fields() const {
return ReducedProductAbstractDomain::get<2>();
}
bool may_this_have_escaped() const {
auto value = ReducedProductAbstractDomain::get<3>();
return !value.get_constant() || *value.get_constant();
}
void mutate_params(std::function<void(ParamDomainEnvironment*)> f) {
apply<0>(std::move(f));
}
void add_read_unwritten_field(DexField* f) {
apply<1>(
[&](ReadUnwrittenFieldDomainEnvironment* domain) { domain->add(f); });
}
void add_written_unread_field(DexField* f) {
apply<2>([&](WrittenUnreadFieldDomainEnvironment* domain) {
domain->unwrap().add(f);
});
}
void set_this_escaped() {
ReducedProductAbstractDomain::apply<3>(
[&](BoolDomain* domain) { *domain = BoolDomain(true); });
}
AnalysisResult get_analysis_result(DexClass* cls) const {
AnalysisResult res{{}, may_this_have_escaped()};
for (auto field : cls->get_ifields()) {
if (get_written_unread_fields().unwrap().contains(field)) {
always_assert(!get_read_unwritten_fields().contains(field));
res.definitely_assigned_ifields.insert(field);
}
}
return res;
}
};
const IRInstruction* get_first_load_param(const cfg::ControlFlowGraph& cfg) {
const auto param_insns = InstructionIterable(cfg.get_param_instructions());
auto& mie = *param_insns.begin();
const auto insn = mie.insn;
always_assert(insn->opcode() == IOPCODE_LOAD_PARAM_OBJECT);
return insn;
}
class Analyzer final : public BaseIRAnalyzer<ConstructorAnalysisEnvironment> {
public:
Analyzer(const cfg::ControlFlowGraph& cfg,
const DexType* declaring_type,
const std::function<const AnalysisResult*(DexMethod*)>&
get_analysis_result)
: BaseIRAnalyzer(cfg),
m_declaring_type(declaring_type),
m_get_analysis_result(get_analysis_result),
m_super_type(type_class(declaring_type)->get_super_class()),
m_first_load_param(get_first_load_param(cfg)) {
MonotonicFixpointIterator::run({});
}
void analyze_instruction(
const IRInstruction* insn,
ConstructorAnalysisEnvironment* current_state) const override {
if (current_state->may_this_have_escaped()) {
// Nothing matters anymore
return;
}
const auto set_current_state_at = [&](reg_t reg, bool wide,
BoolDomain value) {
current_state->mutate_params([&](ParamDomainEnvironment* env) {
env->set(reg, value);
if (wide) {
env->set(reg + 1, BoolDomain::top());
}
});
};
auto opcode = insn->opcode();
if (opcode::is_a_move(opcode)) {
const auto value = current_state->get_params().get(insn->src(0));
set_current_state_at(insn->dest(), insn->dest_is_wide(), value);
return;
}
DexMethod* invoked_ctor_on_this{nullptr};
for (src_index_t src_idx = 0; src_idx < insn->srcs_size(); src_idx++) {
auto src = insn->src(src_idx);
auto param_value = current_state->get_params().get(src);
auto may_be_first_param =
!param_value.get_constant() || *param_value.get_constant();
if (!may_be_first_param) {
continue;
}
if (opcode::is_an_iput(opcode) && src_idx == 1) {
auto field_ref = insn->get_field();
DexField* field = resolve_field(field_ref, FieldSearch::Instance);
if (field != nullptr && field->get_class() == m_declaring_type) {
if (!current_state->get_read_unwritten_fields().contains(field)) {
current_state->add_written_unread_field(field);
}
}
continue;
} else if (opcode::is_an_iget(opcode) && src_idx == 0) {
auto field_ref = insn->get_field();
DexField* field = resolve_field(field_ref, FieldSearch::Instance);
if (field != nullptr && field->get_class() == m_declaring_type) {
if (!current_state->get_written_unread_fields().unwrap().contains(
field)) {
current_state->add_read_unwritten_field(field);
}
}
continue;
} else if (opcode == OPCODE_INVOKE_DIRECT && src_idx == 0) {
auto method_ref = insn->get_method();
if (method::is_init(method_ref)) {
DexMethod* method = resolve_method(method_ref, MethodSearch::Direct);
if (method != nullptr) {
auto method_class = method->get_class();
if (method_class == m_declaring_type ||
method_class == m_super_type) {
invoked_ctor_on_this = method;
continue;
}
}
}
}
// 'this' may have escaped
current_state->set_this_escaped();
return;
}
if (invoked_ctor_on_this) {
// Run this after the loop over the src registers, to make sure we abort
// when the `this` parameter escapes
const auto* analysis_result = m_get_analysis_result(invoked_ctor_on_this);
if (invoked_ctor_on_this->get_class() == m_declaring_type) {
for (auto field : type_class(m_declaring_type)->get_ifields()) {
if (analysis_result->definitely_assigned_ifields.count(field)) {
// If we haven't read the field yet, then we can also mark the field
// as written.
if (!current_state->get_read_unwritten_fields().contains(field)) {
current_state->add_written_unread_field(field);
}
continue;
}
// If not definitely assigned by that ctor, then we can just give up
// here and mark all unwritten fields as read. Later, the intersection
// is computed across all ctors anyway.
if (!current_state->get_written_unread_fields().unwrap().contains(
field)) {
current_state->add_read_unwritten_field(field);
}
}
}
if (analysis_result->may_this_have_escaped) {
current_state->set_this_escaped();
return;
}
}
if (insn->has_dest()) {
bool is_first_parameter = insn == m_first_load_param;
const auto value = BoolDomain(is_first_parameter);
set_current_state_at(insn->dest(), insn->dest_is_wide(), value);
}
}
private:
const DexType* m_declaring_type;
const std::function<const AnalysisResult*(DexMethod*)>& m_get_analysis_result;
const DexType* m_super_type;
const IRInstruction* m_first_load_param;
};
} // namespace
namespace constant_propagation {
namespace definitely_assigned_ifields {
std::unordered_set<const DexField*> get_definitely_assigned_ifields(
const std::unordered_set<const DexType*>& basetype_blocklist,
const Scope& scope) {
Timer t("get_definitely_assigned_ifields");
ConcurrentMap<DexMethod*, std::shared_ptr<AnalysisResult>> analysis_results;
std::function<const AnalysisResult*(DexMethod*)> get_analysis_result;
get_analysis_result = [&](DexMethod* ctor) {
auto res = analysis_results.get(ctor, nullptr);
if (res) {
return res.get();
}
if (ctor->get_code()) {
auto& cfg = ctor->get_code()->cfg();
Analyzer analyzer(cfg, ctor->get_class(), get_analysis_result);
auto env = analyzer.get_exit_state_at(cfg.exit_block());
auto cls = type_class(ctor->get_class());
res = std::make_shared<AnalysisResult>(env.get_analysis_result(cls));
} else {
// Slightly optimistic assumptions: external ctor (without code)
// invocations won't read or write own fields; this is certainly true for
// the most common case, Object::<init>.
res = std::make_shared<AnalysisResult>();
// ... except for ctors of types on the blocklist.
// TODO: Consider using the SummaryGenerator to analyze AOSP classes to
// find external constructors where this may escape.
for (auto basetype : basetype_blocklist) {
if (type::is_subclass(basetype, ctor->get_class())) {
TRACE(CSE, 1, "=== !!! excluded %s", SHOW(ctor));
res->may_this_have_escaped = true;
break;
}
}
}
analysis_results.update(
ctor,
[&](DexMethod*, std::shared_ptr<AnalysisResult>& value, bool exists) {
if (exists) {
always_assert(*value == *res);
res = value;
} else {
value = res;
}
});
return res.get();
};
ConcurrentSet<const DexField*> res;
walk::parallel::classes(scope, [&](DexClass* cls) {
const auto ctors = cls->get_ctors();
if (ctors.empty()) {
// Actually, without a constructor, all fields *are* definitely assigned.
// However, this class is then uninstantiable, and we've another pass that
// effectively deals with that.
return;
}
auto definitely_assigned_ifields = cls->get_ifields();
std20::erase_if(definitely_assigned_ifields, [&](const auto* f) {
return !can_delete(f) || !can_rename(f);
});
for (auto ctor : ctors) {
auto analysis_result = get_analysis_result(ctor);
std20::erase_if(definitely_assigned_ifields, [&](auto* f) {
return !analysis_result->definitely_assigned_ifields.count(f);
});
}
res.insert(definitely_assigned_ifields.begin(),
definitely_assigned_ifields.end());
});
return std::unordered_set<const DexField*>(res.begin(), res.end());
}
} // namespace definitely_assigned_ifields
} // namespace constant_propagation