add cond to compare conversion

This commit is contained in:
water
2020-10-03 18:36:02 -04:00
parent b7cee7b666
commit 0fbffcd409
8 changed files with 311 additions and 56 deletions
+185 -49
View File
@@ -3,32 +3,6 @@
#include "decompiler/Function/CfgVtx.h"
#include "decompiler/Function/Function.h"
std::vector<std::shared_ptr<IR>> IR::get_all_ir(LinkedObjectFile& file) const {
std::vector<std::shared_ptr<IR>> result;
get_children(&result);
size_t last_checked = 0;
size_t last_last_checked = -1;
while (last_checked != last_last_checked) {
last_last_checked = last_checked;
auto end_of_check = result.size();
for (size_t i = last_checked; i < end_of_check; i++) {
auto it = result.at(i).get();
assert(it);
it->get_children(&result);
}
last_checked = end_of_check;
}
// Todo, remove this check which is just for debugging.
std::unordered_set<std::shared_ptr<IR>> unique_ir;
for (auto& x : result) {
unique_ir.insert(x);
}
assert(unique_ir.size() == result.size());
return result;
}
namespace {
std::shared_ptr<IR> cfg_to_ir(Function& f, LinkedObjectFile& file, CfgVtx* vtx);
@@ -60,12 +34,14 @@ void insert_cfg_into_list(Function& f,
output->push_back(got);
}
} else {
// output->push_back(cfg_to_ir(f, file, vtx));
// doesn't look like we're going to get something that can be inlined, so try as usual
auto ir = cfg_to_ir(f, file, vtx);
auto ir_as_begin = dynamic_cast<IR_Begin*>(ir.get());
if (ir_as_begin) {
// we unexpectedly got a begin, even though we didn't think we would. This is okay, but we
// should inline this begin to avoid nested begins.
// should inline this begin to avoid nested begins. This happens in the case where an entire
// control flow pattern is turned into a single op (like type-of) and includes some ops at
// the beginning. We don't have a good way of knowing this will happen until we try it.
for (auto& x : ir_as_begin->forms) {
output->push_back(x);
}
@@ -75,6 +51,12 @@ void insert_cfg_into_list(Function& f,
}
}
/*!
* If it's a begin with a branch as the last operation, returns a pointer to the branch IR
* and also a pointer to the vector which holds the branch operation in its last slot.
* Otherwise returns nullptr. Useful to modify or remove branches found at the end of blocks,
* and inline things into the begin they were found in.
*/
std::pair<IR_Branch*, std::vector<std::shared_ptr<IR>>*> get_condition_branch_as_vector(IR* in) {
auto as_seq = dynamic_cast<IR_Begin*>(in);
if (as_seq) {
@@ -86,6 +68,10 @@ std::pair<IR_Branch*, std::vector<std::shared_ptr<IR>>*> get_condition_branch_as
return std::make_pair(nullptr, nullptr);
}
/*!
* Given an IR, find a branch IR at the end, and also the location of it so it can be patched.
* Returns nullptr as the first item in the pair if it didn't work.
*/
std::pair<IR_Branch*, std::shared_ptr<IR>*> get_condition_branch(std::shared_ptr<IR>* in) {
IR_Branch* condition_branch = dynamic_cast<IR_Branch*>(in->get());
std::shared_ptr<IR>* condition_branch_location = in;
@@ -100,33 +86,137 @@ std::pair<IR_Branch*, std::shared_ptr<IR>*> get_condition_branch(std::shared_ptr
return std::make_pair(condition_branch, condition_branch_location);
}
void clean_up_cond_with_else(IR_CondWithElse* cwe, LinkedObjectFile& file) {
/*!
* Given a CondWithElse IR, remove the internal branches and set the condition to be an actual
* compare IR instead of a branch.
* Doesn't "rebalance" the leading condition because this runs way before expression compaction.
*/
void clean_up_cond_with_else(std::shared_ptr<IR>* ir, LinkedObjectFile& file) {
(void)file;
auto cwe = dynamic_cast<IR_CondWithElse*>(ir->get());
assert(cwe);
for (auto& e : cwe->entries) {
auto jump_to_next = get_condition_branch(&e.condition);
assert(jump_to_next.first);
assert(jump_to_next.first->branch_delay.kind == BranchDelay::NOP);
// printf("got cond condition %s\n", jump_to_next.first->print(file).c_str());
// patch the jump to next with a condition.
auto replacement = std::make_shared<IR_Compare>(jump_to_next.first->condition);
*(jump_to_next.second) = replacement;
// patch the jump at the end of a block.
auto jump_to_end = get_condition_branch(&e.body);
assert(jump_to_end.first);
assert(jump_to_end.first->branch_delay.kind == BranchDelay::NOP);
assert(jump_to_end.first->condition.kind == Condition::ALWAYS);
// if possible, we just want to remove this from the sequence its in.
// but sometimes there's a case with nothing in it so there is no sequence.
// in this case, we can just replace the branch with a NOP IR to indicate that nothing
// happens in this case, but there was still GOAL code to test for it.
// this happens rarely, as you would expect.
auto as_end_of_sequence = get_condition_branch_as_vector(e.body.get());
if (as_end_of_sequence.first) {
assert(as_end_of_sequence.second->size() > 1);
as_end_of_sequence.second->pop_back();
} else {
// this means the case is empty, which is a little bit weird but does actually appear to
// happen in a few places. so we just replace the jump with a nop. In the future we could
// consider having a more explicit "this case is empty" operator so this doesn't get confused
// with an actual MIPS nop.
// In the future we could consider having a more explicit "this case is empty" operator so
// this doesn't get confused with an actual MIPS nop.
*(jump_to_end.second) = std::make_shared<IR_Nop>();
}
}
}
/*!
* A GOAL comparison which produces a boolean is recognized as a cond-no-else by the CFG analysis.
* But it should not be decompiled as a branching statement.
* This either succeeds or asserts and must be called with with something that can be converted
* successfully
*/
void convert_cond_no_else_to_compare(std::shared_ptr<IR>* ir) {
auto cne = dynamic_cast<IR_Cond*>(ir->get());
assert(cne);
auto condition = get_condition_branch(&cne->entries.front().condition);
assert(condition.first);
auto body = dynamic_cast<IR_Set*>(cne->entries.front().body.get());
assert(body);
auto dst = body->dst;
auto src = dynamic_cast<IR_Symbol*>(body->src.get());
assert(src->name == "#f");
assert(cne->entries.size() == 1);
auto condition_as_single = dynamic_cast<IR_Branch*>(cne->entries.front().condition.get());
if (condition_as_single) {
// as far as I can tell this is totally valid but just happens to not appear?
// if this case is ever hit in the future it's fine and we just need to implement this.
// but leaving empty for now so there's fewer things to test.
assert(false);
} else {
auto condition_as_seq = dynamic_cast<IR_Begin*>(cne->entries.front().condition.get());
assert(condition_as_seq);
if (condition_as_seq) {
auto replacement = std::make_shared<IR_Begin>();
replacement->forms = condition_as_seq->forms;
assert(condition.second == &condition_as_seq->forms.back());
replacement->forms.pop_back();
replacement->forms.push_back(std::make_shared<IR_Set>(
IR_Set::REG_64, dst, std::make_shared<IR_Compare>(condition.first->condition)));
*ir = replacement;
}
}
}
/*!
* not yet finished
* Replace internal branches inside a CondNoElse IR.
* If possible will simplify the entire expression into a comparison operation if possible.
* @param ir
* @param file
*/
void clean_up_cond_no_else(std::shared_ptr<IR>* ir, LinkedObjectFile& file) {
auto cne = dynamic_cast<IR_Cond*>(ir->get());
assert(cne);
// for (auto& e : cne->entries) {
for (size_t idx = 0; idx < cne->entries.size(); idx++) {
auto& e = cne->entries.at(idx);
auto jump_to_next = get_condition_branch(&e.condition);
assert(jump_to_next.first);
//
printf("got cond condition %s\n", jump_to_next.first->print(file).c_str());
if (jump_to_next.first->branch_delay.kind == BranchDelay::SET_REG_TRUE &&
cne->entries.size() == 1) {
convert_cond_no_else_to_compare(ir);
} else {
assert(jump_to_next.first->branch_delay.kind == BranchDelay::SET_REG_FALSE ||
jump_to_next.first->branch_delay.kind == BranchDelay::NOP);
}
// auto replacement = std::make_shared<IR_Compare>(jump_to_next.first->condition);
// *(jump_to_next.second) = replacement;
//
// auto jump_to_end = get_condition_branch(&e.body);
// assert(jump_to_end.first);
// assert(jump_to_end.first->branch_delay.kind == BranchDelay::NOP);
// assert(jump_to_end.first->condition.kind == Condition::ALWAYS);
// auto as_end_of_sequence = get_condition_branch_as_vector(e.body.get());
// if (as_end_of_sequence.first) {
// assert(as_end_of_sequence.second->size() > 1);
// as_end_of_sequence.second->pop_back();
// } else {
// // this means the case is empty, which is a little bit weird but does actually appear to
// // happen in a few places. so we just replace the jump with a nop. In the future we
// could
// // consider having a more explicit "this case is empty" operator so this doesn't get
// confused
// // with an actual MIPS nop.
// *(jump_to_end.second) = std::make_shared<IR_Nop>();
// }
}
}
/*!
* Try to convert a short circuiting expression into a "type-of" expression.
* We do this before attempting the normal and/or expressions.
*/
std::shared_ptr<IR> try_sc_as_type_of(Function& f, LinkedObjectFile& file, ShortCircuit* vtx) {
// the assembly looks like this:
/*
@@ -235,9 +325,6 @@ std::shared_ptr<IR> try_sc_as_type_of(Function& f, LinkedObjectFile& file, Short
assert(src_reg3->reg == src_reg->reg);
assert(offset->value == -4);
printf("Candidates for SC type-of:\n%s\n%s\n%s\n", b0_ir->print(file).c_str(),
b1_ir->print(file).c_str(), b2_ir->print(file).c_str());
std::shared_ptr<IR> clobber = nullptr;
if (temp_reg->reg != src_reg->reg && temp_reg->reg != dst_reg->reg) {
clobber = first_branch->condition.src0;
@@ -246,7 +333,6 @@ std::shared_ptr<IR> try_sc_as_type_of(Function& f, LinkedObjectFile& file, Short
return std::make_shared<IR_Set>(IR_Set::REG_64, else_case->dst,
std::make_shared<IR_GetRuntimeType>(shift->arg0, clobber));
} else {
// i'm not brave enough to enable this until I have found a better test case
// remove the branch
b0_ir->forms.pop_back();
// remove the shift
@@ -256,9 +342,11 @@ std::shared_ptr<IR> try_sc_as_type_of(Function& f, LinkedObjectFile& file, Short
IR_Set::REG_64, else_case->dst, std::make_shared<IR_GetRuntimeType>(shift->arg0, clobber)));
return b0_ptr;
}
return nullptr; // todo
}
/*!
* Main CFG vertex to IR conversion. Will pull basic IR ops from the provided function as needed.
*/
std::shared_ptr<IR> cfg_to_ir(Function& f, LinkedObjectFile& file, CfgVtx* vtx) {
if (dynamic_cast<BlockVtx*>(vtx)) {
auto* bv = dynamic_cast<BlockVtx*>(vtx);
@@ -294,30 +382,73 @@ std::shared_ptr<IR> cfg_to_ir(Function& f, LinkedObjectFile& file, CfgVtx* vtx)
return result;
} else if (dynamic_cast<CondWithElse*>(vtx)) {
auto* cvtx = dynamic_cast<CondWithElse*>(vtx);
std::vector<IR_CondWithElse::Entry> entries;
for (auto& x : cvtx->entries) {
IR_CondWithElse::Entry e;
e.condition = cfg_to_ir(f, file, x.condition);
e.body = cfg_to_ir(f, file, x.body);
entries.push_back(std::move(e));
}
// the cfg analysis pass may recognize some things out of order, which can cause
// fake nesting. This is actually a problem at this point because it can turn a normal
// cond into a cond with else, which emits different instructions. This attempts to recognize
// an else which is actually more cases and compacts it into a single statement. At this point
// I don't know if this is sufficient to catch all cases. it may even recognize the wrong
// thing in some cases... maybe we should check the delay slot instead?
auto else_ir = cfg_to_ir(f, file, cvtx->else_vtx);
auto result = std::make_shared<IR_CondWithElse>(entries, else_ir);
clean_up_cond_with_else(result.get(), file);
return result;
if (dynamic_cast<IR_Cond*>(else_ir.get())) {
auto extra_cond = dynamic_cast<IR_Cond*>(else_ir.get());
std::vector<IR_Cond::Entry> entries;
for (auto& x : cvtx->entries) {
IR_Cond::Entry e;
e.condition = cfg_to_ir(f, file, x.condition);
e.body = cfg_to_ir(f, file, x.body);
entries.push_back(std::move(e));
}
for (auto& x : extra_cond->entries) {
entries.push_back(x);
}
std::shared_ptr<IR> result = std::make_shared<IR_Cond>(entries);
clean_up_cond_no_else(&result, file);
return result;
} else {
std::vector<IR_CondWithElse::Entry> entries;
for (auto& x : cvtx->entries) {
IR_CondWithElse::Entry e;
e.condition = cfg_to_ir(f, file, x.condition);
e.body = cfg_to_ir(f, file, x.body);
entries.push_back(std::move(e));
}
std::shared_ptr<IR> result = std::make_shared<IR_CondWithElse>(entries, else_ir);
clean_up_cond_with_else(&result, file);
return result;
}
} else if (dynamic_cast<ShortCircuit*>(vtx)) {
auto* svtx = dynamic_cast<ShortCircuit*>(vtx);
auto as_type_of = try_sc_as_type_of(f, file, svtx);
if (as_type_of) {
return as_type_of;
}
} else if (dynamic_cast<CondNoElse*>(vtx)) {
auto* cvtx = dynamic_cast<CondNoElse*>(vtx);
std::vector<IR_Cond::Entry> entries;
for (auto& x : cvtx->entries) {
IR_Cond::Entry e;
e.condition = cfg_to_ir(f, file, x.condition);
e.body = cfg_to_ir(f, file, x.body);
entries.push_back(std::move(e));
}
std::shared_ptr<IR> result = std::make_shared<IR_Cond>(entries);
clean_up_cond_no_else(&result, file);
return result;
}
throw std::runtime_error("not yet implemented IR conversion.");
return nullptr;
}
/*!
* Post processing pass to clean up while loops - annoyingly the block before a while loop
* has a jump to the condition branch that we need to remove. This currently happens after all
* conversion but this may need to be revisited depending on the final order of simplifications.
*/
void clean_up_while_loops(IR_Begin* sequence, LinkedObjectFile& file) {
(void)file;
std::vector<size_t> to_remove; // the list of branches to remove by index in this sequence
for (size_t i = 0; i < sequence->forms.size(); i++) {
auto* form_as_while = dynamic_cast<IR_WhileLoop*>(sequence->forms.at(i).get());
@@ -353,6 +484,11 @@ void clean_up_while_loops(IR_Begin* sequence, LinkedObjectFile& file) {
}
} // namespace
/*!
* Use a control flow graph to build a single IR representing a function.
* This should be done after basic ops are added and before typing, variable splitting, and
* expression compaction.
*/
std::shared_ptr<IR> build_cfg_ir(Function& function,
ControlFlowGraph& cfg,
LinkedObjectFile& file) {