mirror of
https://github.com/open-goal/jak-project
synced 2026-07-28 07:09:43 -04:00
add cond to compare conversion
This commit is contained in:
+185
-49
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user