| | |
| | |
| |
|
| | #include <algorithm> |
| | #include <array> |
| | #include <optional> |
| | #include <string> |
| | #include <utility> |
| |
|
| | #include <fmt/format.h> |
| |
|
| | #include "common/polyfill_ranges.h" |
| | #include "shader_recompiler/exception.h" |
| | #include "shader_recompiler/frontend/maxwell/control_flow.h" |
| | #include "shader_recompiler/frontend/maxwell/decode.h" |
| | #include "shader_recompiler/frontend/maxwell/indirect_branch_table_track.h" |
| | #include "shader_recompiler/frontend/maxwell/location.h" |
| |
|
| | namespace Shader::Maxwell::Flow { |
| | namespace { |
| | struct Compare { |
| | bool operator()(const Block& lhs, Location rhs) const noexcept { |
| | return lhs.begin < rhs; |
| | } |
| |
|
| | bool operator()(Location lhs, const Block& rhs) const noexcept { |
| | return lhs < rhs.begin; |
| | } |
| |
|
| | bool operator()(const Block& lhs, const Block& rhs) const noexcept { |
| | return lhs.begin < rhs.begin; |
| | } |
| | }; |
| |
|
| | u32 BranchOffset(Location pc, Instruction inst) { |
| | return pc.Offset() + static_cast<u32>(inst.branch.Offset()) + 8u; |
| | } |
| |
|
| | void Split(Block* old_block, Block* new_block, Location pc) { |
| | if (pc <= old_block->begin || pc >= old_block->end) { |
| | throw InvalidArgument("Invalid address to split={}", pc); |
| | } |
| | *new_block = Block{}; |
| | new_block->begin = pc; |
| | new_block->end = old_block->end; |
| | new_block->end_class = old_block->end_class; |
| | new_block->cond = old_block->cond; |
| | new_block->stack = old_block->stack; |
| | new_block->branch_true = old_block->branch_true; |
| | new_block->branch_false = old_block->branch_false; |
| | new_block->function_call = old_block->function_call; |
| | new_block->return_block = old_block->return_block; |
| | new_block->branch_reg = old_block->branch_reg; |
| | new_block->branch_offset = old_block->branch_offset; |
| | new_block->indirect_branches = std::move(old_block->indirect_branches); |
| |
|
| | const Location old_begin{old_block->begin}; |
| | Stack old_stack{std::move(old_block->stack)}; |
| | *old_block = Block{}; |
| | old_block->begin = old_begin; |
| | old_block->end = pc; |
| | old_block->end_class = EndClass::Branch; |
| | old_block->cond = IR::Condition(true); |
| | old_block->stack = old_stack; |
| | old_block->branch_true = new_block; |
| | old_block->branch_false = nullptr; |
| | } |
| |
|
| | Token OpcodeToken(Opcode opcode) { |
| | switch (opcode) { |
| | case Opcode::PBK: |
| | case Opcode::BRK: |
| | return Token::PBK; |
| | case Opcode::PCNT: |
| | case Opcode::CONT: |
| | return Token::PCNT; |
| | case Opcode::PEXIT: |
| | case Opcode::EXIT: |
| | return Token::PEXIT; |
| | case Opcode::PLONGJMP: |
| | case Opcode::LONGJMP: |
| | return Token::PLONGJMP; |
| | case Opcode::PRET: |
| | case Opcode::RET: |
| | case Opcode::CAL: |
| | return Token::PRET; |
| | case Opcode::SSY: |
| | case Opcode::SYNC: |
| | return Token::SSY; |
| | default: |
| | throw InvalidArgument("{}", opcode); |
| | } |
| | } |
| |
|
| | bool IsAbsoluteJump(Opcode opcode) { |
| | switch (opcode) { |
| | case Opcode::JCAL: |
| | case Opcode::JMP: |
| | case Opcode::JMX: |
| | return true; |
| | default: |
| | return false; |
| | } |
| | } |
| |
|
| | bool HasFlowTest(Opcode opcode) { |
| | switch (opcode) { |
| | case Opcode::BRA: |
| | case Opcode::BRX: |
| | case Opcode::EXIT: |
| | case Opcode::JMP: |
| | case Opcode::JMX: |
| | case Opcode::KIL: |
| | case Opcode::BRK: |
| | case Opcode::CONT: |
| | case Opcode::LONGJMP: |
| | case Opcode::RET: |
| | case Opcode::SYNC: |
| | return true; |
| | case Opcode::CAL: |
| | case Opcode::JCAL: |
| | return false; |
| | default: |
| | throw InvalidArgument("Invalid branch {}", opcode); |
| | } |
| | } |
| |
|
| | std::string NameOf(const Block& block) { |
| | if (block.begin.IsVirtual()) { |
| | return fmt::format("\"Virtual {}\"", block.begin); |
| | } else { |
| | return fmt::format("\"{}\"", block.begin); |
| | } |
| | } |
| | } |
| |
|
| | void Stack::Push(Token token, Location target) { |
| | entries.push_back({ |
| | .token = token, |
| | .target{target}, |
| | }); |
| | } |
| |
|
| | std::pair<Location, Stack> Stack::Pop(Token token) const { |
| | const std::optional<Location> pc{Peek(token)}; |
| | if (!pc) { |
| | throw LogicError("Token could not be found"); |
| | } |
| | return {*pc, Remove(token)}; |
| | } |
| |
|
| | std::optional<Location> Stack::Peek(Token token) const { |
| | const auto it{std::find_if(entries.rbegin(), entries.rend(), |
| | [token](const auto& entry) { return entry.token == token; })}; |
| | if (it == entries.rend()) { |
| | return std::nullopt; |
| | } |
| | return it->target; |
| | } |
| |
|
| | Stack Stack::Remove(Token token) const { |
| | const auto it{std::find_if(entries.rbegin(), entries.rend(), |
| | [token](const auto& entry) { return entry.token == token; })}; |
| | const auto pos{std::distance(entries.rbegin(), it)}; |
| | Stack result; |
| | result.entries.insert(result.entries.end(), entries.begin(), entries.end() - pos - 1); |
| | return result; |
| | } |
| |
|
| | bool Block::Contains(Location pc) const noexcept { |
| | return pc >= begin && pc < end; |
| | } |
| |
|
| | Function::Function(ObjectPool<Block>& block_pool, Location start_address) |
| | : entrypoint{start_address} { |
| | Label& label{labels.emplace_back()}; |
| | label.address = start_address; |
| | label.block = block_pool.Create(Block{}); |
| | label.block->begin = start_address; |
| | label.block->end = start_address; |
| | label.block->end_class = EndClass::Branch; |
| | label.block->cond = IR::Condition(true); |
| | label.block->branch_true = nullptr; |
| | label.block->branch_false = nullptr; |
| | } |
| |
|
| | CFG::CFG(Environment& env_, ObjectPool<Block>& block_pool_, Location start_address, |
| | bool exits_to_dispatcher_) |
| | : env{env_}, block_pool{block_pool_}, program_start{start_address}, exits_to_dispatcher{ |
| | exits_to_dispatcher_} { |
| | if (exits_to_dispatcher) { |
| | dispatch_block = block_pool.Create(Block{}); |
| | dispatch_block->begin = {}; |
| | dispatch_block->end = {}; |
| | dispatch_block->end_class = EndClass::Exit; |
| | dispatch_block->cond = IR::Condition(true); |
| | dispatch_block->stack = {}; |
| | dispatch_block->branch_true = nullptr; |
| | dispatch_block->branch_false = nullptr; |
| | } |
| | functions.emplace_back(block_pool, start_address); |
| | for (FunctionId function_id = 0; function_id < functions.size(); ++function_id) { |
| | while (!functions[function_id].labels.empty()) { |
| | Function& function{functions[function_id]}; |
| | Label label{function.labels.back()}; |
| | function.labels.pop_back(); |
| | AnalyzeLabel(function_id, label); |
| | } |
| | } |
| | if (exits_to_dispatcher) { |
| | const auto last_block{functions[0].blocks.rbegin()}; |
| | dispatch_block->begin = last_block->end + 1; |
| | dispatch_block->end = last_block->end + 1; |
| | functions[0].blocks.insert(*dispatch_block); |
| | } |
| | } |
| |
|
| | void CFG::AnalyzeLabel(FunctionId function_id, Label& label) { |
| | if (InspectVisitedBlocks(function_id, label)) { |
| | |
| | return; |
| | } |
| | |
| | Function* const function{&functions[function_id]}; |
| | Location pc{label.address}; |
| | const auto next_it{function->blocks.upper_bound(pc, Compare{})}; |
| | const bool is_last{next_it == function->blocks.end()}; |
| | Block* const next{is_last ? nullptr : &*next_it}; |
| | |
| | Block* const block{label.block}; |
| | |
| | bool is_branch{false}; |
| | while (!next || pc < next->begin) { |
| | is_branch = AnalyzeInst(block, function_id, pc) == AnalysisState::Branch; |
| | if (is_branch) { |
| | break; |
| | } |
| | ++pc; |
| | } |
| | if (!is_branch) { |
| | |
| | |
| | block->end = pc; |
| | block->cond = IR::Condition{true}; |
| | block->branch_true = next; |
| | block->branch_false = nullptr; |
| | } |
| | |
| | |
| | functions[function_id].blocks.insert(*block); |
| | } |
| |
|
| | bool CFG::InspectVisitedBlocks(FunctionId function_id, const Label& label) { |
| | const Location pc{label.address}; |
| | Function& function{functions[function_id]}; |
| | const auto it{ |
| | std::ranges::find_if(function.blocks, [pc](auto& block) { return block.Contains(pc); })}; |
| | if (it == function.blocks.end()) { |
| | |
| | return false; |
| | } |
| | Block* const visited_block{&*it}; |
| | if (visited_block->begin == pc) { |
| | throw LogicError("Dangling block"); |
| | } |
| | Block* const new_block{label.block}; |
| | Split(visited_block, new_block, pc); |
| | function.blocks.insert(it, *new_block); |
| | return true; |
| | } |
| |
|
| | CFG::AnalysisState CFG::AnalyzeInst(Block* block, FunctionId function_id, Location pc) { |
| | const Instruction inst{env.ReadInstruction(pc.Offset())}; |
| | const Opcode opcode{Decode(inst.raw)}; |
| | switch (opcode) { |
| | case Opcode::BRA: |
| | case Opcode::JMP: |
| | case Opcode::RET: |
| | if (!AnalyzeBranch(block, function_id, pc, inst, opcode)) { |
| | return AnalysisState::Continue; |
| | } |
| | switch (opcode) { |
| | case Opcode::BRA: |
| | case Opcode::JMP: |
| | AnalyzeBRA(block, function_id, pc, inst, IsAbsoluteJump(opcode)); |
| | break; |
| | case Opcode::RET: |
| | block->end_class = EndClass::Return; |
| | break; |
| | default: |
| | break; |
| | } |
| | block->end = pc; |
| | return AnalysisState::Branch; |
| | case Opcode::BRK: |
| | case Opcode::CONT: |
| | case Opcode::LONGJMP: |
| | case Opcode::SYNC: { |
| | if (!AnalyzeBranch(block, function_id, pc, inst, opcode)) { |
| | return AnalysisState::Continue; |
| | } |
| | const auto [stack_pc, new_stack]{block->stack.Pop(OpcodeToken(opcode))}; |
| | block->branch_true = AddLabel(block, new_stack, stack_pc, function_id); |
| | block->end = pc; |
| | return AnalysisState::Branch; |
| | } |
| | case Opcode::KIL: { |
| | const Predicate pred{inst.Pred()}; |
| | const auto ir_pred{static_cast<IR::Pred>(pred.index)}; |
| | const IR::Condition cond{inst.branch.flow_test, ir_pred, pred.negated}; |
| | AnalyzeCondInst(block, function_id, pc, EndClass::Kill, cond); |
| | return AnalysisState::Branch; |
| | } |
| | case Opcode::PBK: |
| | case Opcode::PCNT: |
| | case Opcode::PEXIT: |
| | case Opcode::PLONGJMP: |
| | case Opcode::SSY: |
| | block->stack.Push(OpcodeToken(opcode), BranchOffset(pc, inst)); |
| | return AnalysisState::Continue; |
| | case Opcode::BRX: |
| | case Opcode::JMX: |
| | return AnalyzeBRX(block, pc, inst, IsAbsoluteJump(opcode), function_id); |
| | case Opcode::EXIT: |
| | return AnalyzeEXIT(block, function_id, pc, inst); |
| | case Opcode::PRET: |
| | throw NotImplementedException("PRET flow analysis"); |
| | case Opcode::CAL: |
| | case Opcode::JCAL: { |
| | const bool is_absolute{IsAbsoluteJump(opcode)}; |
| | const Location cal_pc{is_absolute ? inst.branch.Absolute() : BranchOffset(pc, inst)}; |
| | |
| | |
| | const auto it{std::ranges::find(functions, cal_pc, &Function::entrypoint)}; |
| | const bool exists{it != functions.end()}; |
| | const FunctionId call_id{exists ? static_cast<size_t>(std::distance(functions.begin(), it)) |
| | : functions.size()}; |
| | if (!exists) { |
| | functions.emplace_back(block_pool, cal_pc); |
| | } |
| | block->end_class = EndClass::Call; |
| | block->function_call = call_id; |
| | block->return_block = AddLabel(block, block->stack, pc + 1, function_id); |
| | block->end = pc; |
| | return AnalysisState::Branch; |
| | } |
| | default: |
| | break; |
| | } |
| | const Predicate pred{inst.Pred()}; |
| | if (pred == Predicate{true} || pred == Predicate{false}) { |
| | return AnalysisState::Continue; |
| | } |
| | const IR::Condition cond{static_cast<IR::Pred>(pred.index), pred.negated}; |
| | AnalyzeCondInst(block, function_id, pc, EndClass::Branch, cond); |
| | return AnalysisState::Branch; |
| | } |
| |
|
| | void CFG::AnalyzeCondInst(Block* block, FunctionId function_id, Location pc, |
| | EndClass insn_end_class, IR::Condition cond) { |
| | if (block->begin != pc) { |
| | |
| | |
| | block->end = pc; |
| | block->cond = IR::Condition{true}; |
| | block->branch_true = AddLabel(block, block->stack, pc, function_id); |
| | block->branch_false = nullptr; |
| | return; |
| | } |
| | |
| | Block* const conditional_block{block_pool.Create()}; |
| | Block virtual_block{}; |
| | virtual_block.begin = block->begin.Virtual(); |
| | virtual_block.end = block->begin.Virtual(); |
| | virtual_block.end_class = EndClass::Branch; |
| | virtual_block.stack = block->stack; |
| | virtual_block.cond = cond; |
| | virtual_block.branch_true = conditional_block; |
| | virtual_block.branch_false = nullptr; |
| | |
| | *conditional_block = std::move(*block); |
| | |
| | *block = std::move(virtual_block); |
| | |
| | conditional_block->end = pc + 1; |
| | conditional_block->end_class = insn_end_class; |
| | |
| | Block* const endif_block{AddLabel(conditional_block, block->stack, pc + 1, function_id)}; |
| | |
| | block->branch_false = endif_block; |
| | |
| | |
| | |
| | if (insn_end_class == EndClass::Branch || insn_end_class == EndClass::Kill) { |
| | conditional_block->cond = IR::Condition{true}; |
| | conditional_block->branch_true = endif_block; |
| | conditional_block->branch_false = nullptr; |
| | } |
| | |
| | functions[function_id].blocks.insert(*conditional_block); |
| | } |
| |
|
| | bool CFG::AnalyzeBranch(Block* block, FunctionId function_id, Location pc, Instruction inst, |
| | Opcode opcode) { |
| | if (inst.branch.is_cbuf) { |
| | throw NotImplementedException("Branch with constant buffer offset"); |
| | } |
| | const Predicate pred{inst.Pred()}; |
| | if (pred == Predicate{false}) { |
| | return false; |
| | } |
| | const bool has_flow_test{HasFlowTest(opcode)}; |
| | const IR::FlowTest flow_test{has_flow_test ? inst.branch.flow_test.Value() : IR::FlowTest::T}; |
| | if (pred != Predicate{true} || flow_test != IR::FlowTest::T) { |
| | block->cond = IR::Condition(flow_test, static_cast<IR::Pred>(pred.index), pred.negated); |
| | block->branch_false = AddLabel(block, block->stack, pc + 1, function_id); |
| | } else { |
| | block->cond = IR::Condition{true}; |
| | } |
| | return true; |
| | } |
| |
|
| | void CFG::AnalyzeBRA(Block* block, FunctionId function_id, Location pc, Instruction inst, |
| | bool is_absolute) { |
| | const Location bra_pc{is_absolute ? inst.branch.Absolute() : BranchOffset(pc, inst)}; |
| | block->branch_true = AddLabel(block, block->stack, bra_pc, function_id); |
| | } |
| |
|
| | CFG::AnalysisState CFG::AnalyzeBRX(Block* block, Location pc, Instruction inst, bool is_absolute, |
| | FunctionId function_id) { |
| | const std::optional brx_table{TrackIndirectBranchTable(env, pc, program_start)}; |
| | if (!brx_table) { |
| | TrackIndirectBranchTable(env, pc, program_start); |
| | throw NotImplementedException("Failed to track indirect branch"); |
| | } |
| | const IR::FlowTest flow_test{inst.branch.flow_test}; |
| | const Predicate pred{inst.Pred()}; |
| | if (flow_test != IR::FlowTest::T || pred != Predicate{true}) { |
| | throw NotImplementedException("Conditional indirect branch"); |
| | } |
| | std::vector<u32> targets; |
| | targets.reserve(brx_table->num_entries); |
| | for (u32 i = 0; i < brx_table->num_entries; ++i) { |
| | u32 target{env.ReadCbufValue(brx_table->cbuf_index, brx_table->cbuf_offset + i * 4)}; |
| | if (!is_absolute) { |
| | target += pc.Offset(); |
| | } |
| | target += static_cast<u32>(brx_table->branch_offset); |
| | target += 8; |
| | targets.push_back(target); |
| | } |
| | std::ranges::sort(targets); |
| | targets.erase(std::unique(targets.begin(), targets.end()), targets.end()); |
| |
|
| | block->indirect_branches.reserve(targets.size()); |
| | for (const u32 target : targets) { |
| | Block* const branch{AddLabel(block, block->stack, target, function_id)}; |
| | block->indirect_branches.push_back({ |
| | .block = branch, |
| | .address = target, |
| | }); |
| | } |
| | block->cond = IR::Condition{true}; |
| | block->end = pc + 1; |
| | block->end_class = EndClass::IndirectBranch; |
| | block->branch_reg = brx_table->branch_reg; |
| | block->branch_offset = brx_table->branch_offset + 8; |
| | if (!is_absolute) { |
| | block->branch_offset += pc.Offset(); |
| | } |
| | return AnalysisState::Branch; |
| | } |
| |
|
| | CFG::AnalysisState CFG::AnalyzeEXIT(Block* block, FunctionId function_id, Location pc, |
| | Instruction inst) { |
| | const IR::FlowTest flow_test{inst.branch.flow_test}; |
| | const Predicate pred{inst.Pred()}; |
| | if (pred == Predicate{false} || flow_test == IR::FlowTest::F) { |
| | |
| | return AnalysisState::Continue; |
| | } |
| | if (exits_to_dispatcher && function_id != 0) { |
| | throw NotImplementedException("Dispatch EXIT on external function"); |
| | } |
| | if (pred != Predicate{true} || flow_test != IR::FlowTest::T) { |
| | if (block->stack.Peek(Token::PEXIT).has_value()) { |
| | throw NotImplementedException("Conditional EXIT with PEXIT token"); |
| | } |
| | const IR::Condition cond{flow_test, static_cast<IR::Pred>(pred.index), pred.negated}; |
| | if (exits_to_dispatcher) { |
| | block->end = pc; |
| | block->end_class = EndClass::Branch; |
| | block->cond = cond; |
| | block->branch_true = dispatch_block; |
| | block->branch_false = AddLabel(block, block->stack, pc + 1, function_id); |
| | return AnalysisState::Branch; |
| | } |
| | AnalyzeCondInst(block, function_id, pc, EndClass::Exit, cond); |
| | return AnalysisState::Branch; |
| | } |
| | if (const std::optional<Location> exit_pc{block->stack.Peek(Token::PEXIT)}) { |
| | const Stack popped_stack{block->stack.Remove(Token::PEXIT)}; |
| | block->cond = IR::Condition{true}; |
| | block->branch_true = AddLabel(block, popped_stack, *exit_pc, function_id); |
| | block->branch_false = nullptr; |
| | return AnalysisState::Branch; |
| | } |
| | if (exits_to_dispatcher) { |
| | block->cond = IR::Condition{true}; |
| | block->end = pc; |
| | block->end_class = EndClass::Branch; |
| | block->branch_true = dispatch_block; |
| | block->branch_false = nullptr; |
| | return AnalysisState::Branch; |
| | } |
| | block->end = pc + 1; |
| | block->end_class = EndClass::Exit; |
| | return AnalysisState::Branch; |
| | } |
| |
|
| | Block* CFG::AddLabel(Block* block, Stack stack, Location pc, FunctionId function_id) { |
| | Function& function{functions[function_id]}; |
| | if (block->begin == pc) { |
| | |
| | return block; |
| | } |
| | if (const auto it{function.blocks.find(pc, Compare{})}; it != function.blocks.end()) { |
| | |
| | if (function.blocks.begin() != it) { |
| | |
| | |
| | |
| | |
| | const auto prev{std::prev(it)}; |
| | if (it->begin.Virtual() == prev->begin) { |
| | return &*prev; |
| | } |
| | } |
| | return &*it; |
| | } |
| | |
| | const auto label_it{std::ranges::find(function.labels, pc, &Label::address)}; |
| | if (label_it != function.labels.end()) { |
| | return label_it->block; |
| | } |
| | Block* const new_block{block_pool.Create()}; |
| | new_block->begin = pc; |
| | new_block->end = pc; |
| | new_block->end_class = EndClass::Branch; |
| | new_block->cond = IR::Condition(true); |
| | new_block->stack = stack; |
| | new_block->branch_true = nullptr; |
| | new_block->branch_false = nullptr; |
| | function.labels.push_back(Label{ |
| | .address{pc}, |
| | .block = new_block, |
| | .stack{std::move(stack)}, |
| | }); |
| | return new_block; |
| | } |
| |
|
| | std::string CFG::Dot() const { |
| | int node_uid{0}; |
| |
|
| | std::string dot{"digraph shader {\n"}; |
| | for (const Function& function : functions) { |
| | dot += fmt::format("\tsubgraph cluster_{} {{\n", function.entrypoint); |
| | dot += fmt::format("\t\tnode [style=filled];\n"); |
| | for (const Block& block : function.blocks) { |
| | const std::string name{NameOf(block)}; |
| | const auto add_branch = [&](Block* branch, bool add_label) { |
| | dot += fmt::format("\t\t{}->{}", name, NameOf(*branch)); |
| | if (add_label && block.cond != IR::Condition{true} && |
| | block.cond != IR::Condition{false}) { |
| | dot += fmt::format(" [label=\"{}\"]", block.cond); |
| | } |
| | dot += '\n'; |
| | }; |
| | dot += fmt::format("\t\t{};\n", name); |
| | switch (block.end_class) { |
| | case EndClass::Branch: |
| | if (block.cond != IR::Condition{false}) { |
| | add_branch(block.branch_true, true); |
| | } |
| | if (block.cond != IR::Condition{true}) { |
| | add_branch(block.branch_false, false); |
| | } |
| | break; |
| | case EndClass::IndirectBranch: |
| | for (const IndirectBranch& branch : block.indirect_branches) { |
| | add_branch(branch.block, false); |
| | } |
| | break; |
| | case EndClass::Call: |
| | dot += fmt::format("\t\t{}->N{};\n", name, node_uid); |
| | dot += fmt::format("\t\tN{}->{};\n", node_uid, NameOf(*block.return_block)); |
| | dot += fmt::format("\t\tN{} [label=\"Call {}\"][shape=square][style=stripped];\n", |
| | node_uid, block.function_call); |
| | dot += '\n'; |
| | ++node_uid; |
| | break; |
| | case EndClass::Exit: |
| | dot += fmt::format("\t\t{}->N{};\n", name, node_uid); |
| | dot += fmt::format("\t\tN{} [label=\"Exit\"][shape=square][style=stripped];\n", |
| | node_uid); |
| | ++node_uid; |
| | break; |
| | case EndClass::Return: |
| | dot += fmt::format("\t\t{}->N{};\n", name, node_uid); |
| | dot += fmt::format("\t\tN{} [label=\"Return\"][shape=square][style=stripped];\n", |
| | node_uid); |
| | ++node_uid; |
| | break; |
| | case EndClass::Kill: |
| | dot += fmt::format("\t\t{}->N{};\n", name, node_uid); |
| | dot += fmt::format("\t\tN{} [label=\"Kill\"][shape=square][style=stripped];\n", |
| | node_uid); |
| | ++node_uid; |
| | break; |
| | } |
| | } |
| | if (function.entrypoint == 8) { |
| | dot += fmt::format("\t\tlabel = \"main\";\n"); |
| | } else { |
| | dot += fmt::format("\t\tlabel = \"Function {}\";\n", function.entrypoint); |
| | } |
| | dot += "\t}\n"; |
| | } |
| | if (!functions.empty()) { |
| | auto& function{functions.front()}; |
| | if (function.blocks.empty()) { |
| | dot += "Start;\n"; |
| | } else { |
| | dot += fmt::format("\tStart -> {};\n", NameOf(*function.blocks.begin())); |
| | } |
| | dot += fmt::format("\tStart [shape=diamond];\n"); |
| | } |
| | dot += "}\n"; |
| | return dot; |
| | } |
| |
|
| | } |
| |
|