/* SPDX-License-Identifier: GPL-2.0-or-later */ #include "FN_multi_function_procedure.hh" #include "BLI_dot_export.hh" #include "BLI_stack.hh" namespace blender::fn::multi_function { void InstructionCursor::set_next(Procedure &procedure, Instruction *new_instruction) const { switch (type_) { case Type::None: { break; } case Type::Entry: { procedure.set_entry(*new_instruction); break; } case Type::Call: { static_cast(instruction_)->set_next(new_instruction); break; } case Type::Branch: { BranchInstruction &branch_instruction = *static_cast(instruction_); if (branch_output_) { branch_instruction.set_branch_true(new_instruction); } else { branch_instruction.set_branch_false(new_instruction); } break; } case Type::Destruct: { static_cast(instruction_)->set_next(new_instruction); break; } case Type::Dummy: { static_cast(instruction_)->set_next(new_instruction); break; } } } Instruction *InstructionCursor::next(Procedure &procedure) const { switch (type_) { case Type::None: return nullptr; case Type::Entry: return procedure.entry(); case Type::Call: return static_cast(instruction_)->next(); case Type::Branch: { BranchInstruction &branch_instruction = *static_cast(instruction_); if (branch_output_) { return branch_instruction.branch_true(); } return branch_instruction.branch_false(); } case Type::Destruct: return static_cast(instruction_)->next(); case Type::Dummy: return static_cast(instruction_)->next(); } return nullptr; } void Variable::set_name(std::string name) { name_ = std::move(name); } void CallInstruction::set_next(Instruction *instruction) { if (next_ != nullptr) { next_->prev_.remove_first_occurrence_and_reorder(*this); } if (instruction != nullptr) { instruction->prev_.append(*this); } next_ = instruction; } void CallInstruction::set_param_variable(int param_index, Variable *variable) { if (params_[param_index] != nullptr) { params_[param_index]->users_.remove_first_occurrence_and_reorder(this); } if (variable != nullptr) { BLI_assert(fn_->param_type(param_index).data_type() == variable->data_type()); variable->users_.append(this); } params_[param_index] = variable; } void CallInstruction::set_params(Span variables) { BLI_assert(variables.size() == params_.size()); for (const int i : variables.index_range()) { this->set_param_variable(i, variables[i]); } } void BranchInstruction::set_condition(Variable *variable) { if (condition_ != nullptr) { condition_->users_.remove_first_occurrence_and_reorder(this); } if (variable != nullptr) { variable->users_.append(this); } condition_ = variable; } void BranchInstruction::set_branch_true(Instruction *instruction) { if (branch_true_ != nullptr) { branch_true_->prev_.remove_first_occurrence_and_reorder({*this, true}); } if (instruction != nullptr) { instruction->prev_.append({*this, true}); } branch_true_ = instruction; } void BranchInstruction::set_branch_false(Instruction *instruction) { if (branch_false_ != nullptr) { branch_false_->prev_.remove_first_occurrence_and_reorder({*this, false}); } if (instruction != nullptr) { instruction->prev_.append({*this, false}); } branch_false_ = instruction; } void DestructInstruction::set_variable(Variable *variable) { if (variable_ != nullptr) { variable_->users_.remove_first_occurrence_and_reorder(this); } if (variable != nullptr) { variable->users_.append(this); } variable_ = variable; } void DestructInstruction::set_next(Instruction *instruction) { if (next_ != nullptr) { next_->prev_.remove_first_occurrence_and_reorder(*this); } if (instruction != nullptr) { instruction->prev_.append(*this); } next_ = instruction; } void DummyInstruction::set_next(Instruction *instruction) { if (next_ != nullptr) { next_->prev_.remove_first_occurrence_and_reorder(*this); } if (instruction != nullptr) { instruction->prev_.append(*this); } next_ = instruction; } Variable &Procedure::new_variable(DataType data_type, std::string name) { Variable &variable = *allocator_.construct().release(); variable.name_ = std::move(name); variable.data_type_ = data_type; variable.index_in_graph_ = variables_.size(); variables_.append(&variable); return variable; } CallInstruction &Procedure::new_call_instruction(const MultiFunction &fn) { CallInstruction &instruction = *allocator_.construct().release(); instruction.type_ = InstructionType::Call; instruction.fn_ = &fn; instruction.params_ = allocator_.allocate_array(fn.param_amount()); instruction.params_.fill(nullptr); call_instructions_.append(&instruction); return instruction; } BranchInstruction &Procedure::new_branch_instruction() { BranchInstruction &instruction = *allocator_.construct().release(); instruction.type_ = InstructionType::Branch; branch_instructions_.append(&instruction); return instruction; } DestructInstruction &Procedure::new_destruct_instruction() { DestructInstruction &instruction = *allocator_.construct().release(); instruction.type_ = InstructionType::Destruct; destruct_instructions_.append(&instruction); return instruction; } DummyInstruction &Procedure::new_dummy_instruction() { DummyInstruction &instruction = *allocator_.construct().release(); instruction.type_ = InstructionType::Dummy; dummy_instructions_.append(&instruction); return instruction; } ReturnInstruction &Procedure::new_return_instruction() { ReturnInstruction &instruction = *allocator_.construct().release(); instruction.type_ = InstructionType::Return; return_instructions_.append(&instruction); return instruction; } void Procedure::add_parameter(ParamType::InterfaceType interface_type, Variable &variable) { params_.append({interface_type, &variable}); } void Procedure::set_entry(Instruction &entry) { if (entry_ != nullptr) { entry_->prev_.remove_first_occurrence_and_reorder(InstructionCursor::ForEntry()); } entry_ = &entry; entry_->prev_.append(InstructionCursor::ForEntry()); } Procedure::~Procedure() { for (CallInstruction *instruction : call_instructions_) { instruction->~CallInstruction(); } for (BranchInstruction *instruction : branch_instructions_) { instruction->~BranchInstruction(); } for (DestructInstruction *instruction : destruct_instructions_) { instruction->~DestructInstruction(); } for (DummyInstruction *instruction : dummy_instructions_) { instruction->~DummyInstruction(); } for (ReturnInstruction *instruction : return_instructions_) { instruction->~ReturnInstruction(); } for (Variable *variable : variables_) { variable->~Variable(); } } bool Procedure::validate() const { if (entry_ == nullptr) { return false; } if (!this->validate_all_instruction_pointers_set()) { return false; } if (!this->validate_all_params_provided()) { return false; } if (!this->validate_same_variables_in_one_call()) { return false; } if (!this->validate_parameters()) { return false; } if (!this->validate_initialization()) { return false; } return true; } bool Procedure::validate_all_instruction_pointers_set() const { for (const CallInstruction *instruction : call_instructions_) { if (instruction->next_ == nullptr) { return false; } } for (const DestructInstruction *instruction : destruct_instructions_) { if (instruction->next_ == nullptr) { return false; } } for (const BranchInstruction *instruction : branch_instructions_) { if (instruction->branch_true_ == nullptr) { return false; } if (instruction->branch_false_ == nullptr) { return false; } } for (const DummyInstruction *instruction : dummy_instructions_) { if (instruction->next_ == nullptr) { return false; } } return true; } bool Procedure::validate_all_params_provided() const { for (const CallInstruction *instruction : call_instructions_) { const MultiFunction &fn = instruction->fn(); for (const int param_index : fn.param_indices()) { const ParamType param_type = fn.param_type(param_index); if (param_type.category() == ParamCategory::SingleOutput) { /* Single outputs are optional. */ continue; } const Variable *variable = instruction->params_[param_index]; if (variable == nullptr) { return false; } } } for (const BranchInstruction *instruction : branch_instructions_) { if (instruction->condition_ == nullptr) { return false; } } for (const DestructInstruction *instruction : destruct_instructions_) { if (instruction->variable_ == nullptr) { return false; } } return true; } bool Procedure::validate_same_variables_in_one_call() const { for (const CallInstruction *instruction : call_instructions_) { const MultiFunction &fn = *instruction->fn_; for (const int param_index : fn.param_indices()) { const ParamType param_type = fn.param_type(param_index); const Variable *variable = instruction->params_[param_index]; if (variable == nullptr) { continue; } for (const int other_param_index : fn.param_indices()) { if (other_param_index == param_index) { continue; } const Variable *other_variable = instruction->params_[other_param_index]; if (other_variable != variable) { continue; } if (ELEM(param_type.interface_type(), ParamType::Mutable, ParamType::Output)) { /* When a variable is used as mutable or output parameter, it can only be used once. */ return false; } const ParamType other_param_type = fn.param_type(other_param_index); /* A variable is allowed to be used as input more than once. */ if (other_param_type.interface_type() != ParamType::Input) { return false; } } } } return true; } bool Procedure::validate_parameters() const { Set variables; for (const Parameter ¶m : params_) { /* One variable cannot be used as multiple parameters. */ if (!variables.add(param.variable)) { return false; } } return true; } bool Procedure::validate_initialization() const { /* TODO: Issue warning when it maybe wrongly initialized. */ for (const DestructInstruction *instruction : destruct_instructions_) { const Variable &variable = *instruction->variable_; const InitState state = this->find_initialization_state_before_instruction(*instruction, variable); if (!state.can_be_initialized) { return false; } } for (const BranchInstruction *instruction : branch_instructions_) { const Variable &variable = *instruction->condition_; const InitState state = this->find_initialization_state_before_instruction(*instruction, variable); if (!state.can_be_initialized) { return false; } } for (const CallInstruction *instruction : call_instructions_) { const MultiFunction &fn = *instruction->fn_; for (const int param_index : fn.param_indices()) { const ParamType param_type = fn.param_type(param_index); /* If the parameter was an unneeded output, it could be null. */ if (!instruction->params_[param_index]) { continue; } const Variable &variable = *instruction->params_[param_index]; const InitState state = this->find_initialization_state_before_instruction(*instruction, variable); switch (param_type.interface_type()) { case ParamType::Input: case ParamType::Mutable: { if (!state.can_be_initialized) { return false; } break; } case ParamType::Output: { if (!state.can_be_uninitialized) { return false; } break; } } } } Set variables_that_should_be_initialized_on_return; for (const Parameter ¶m : params_) { if (ELEM(param.type, ParamType::Mutable, ParamType::Output)) { variables_that_should_be_initialized_on_return.add_new(param.variable); } } for (const ReturnInstruction *instruction : return_instructions_) { for (const Variable *variable : variables_) { const InitState init_state = this->find_initialization_state_before_instruction(*instruction, *variable); if (variables_that_should_be_initialized_on_return.contains(variable)) { if (!init_state.can_be_initialized) { return false; } } else { if (!init_state.can_be_uninitialized) { return false; } } } } return true; } Procedure::InitState Procedure::find_initialization_state_before_instruction( const Instruction &target_instruction, const Variable &target_variable) const { InitState state; auto check_entry_instruction = [&]() { bool caller_initialized_variable = false; for (const Parameter ¶m : params_) { if (param.variable == &target_variable) { if (ELEM(param.type, ParamType::Input, ParamType::Mutable)) { caller_initialized_variable = true; break; } } } if (caller_initialized_variable) { state.can_be_initialized = true; } else { state.can_be_uninitialized = true; } }; if (&target_instruction == entry_) { check_entry_instruction(); } Set checked_instructions; Stack instructions_to_check; for (const InstructionCursor &cursor : target_instruction.prev_) { if (cursor.instruction() != nullptr) { instructions_to_check.push(cursor.instruction()); } } while (!instructions_to_check.is_empty()) { const Instruction &instruction = *instructions_to_check.pop(); if (!checked_instructions.add(&instruction)) { /* Skip if the instruction has been checked already. */ continue; } bool state_modified = false; switch (instruction.type_) { case InstructionType::Call: { const CallInstruction &call_instruction = static_cast( instruction); const MultiFunction &fn = *call_instruction.fn_; for (const int param_index : fn.param_indices()) { if (call_instruction.params_[param_index] == &target_variable) { const ParamType param_type = fn.param_type(param_index); if (param_type.interface_type() == ParamType::Output) { state.can_be_initialized = true; state_modified = true; break; } } } break; } case InstructionType::Destruct: { const DestructInstruction &destruct_instruction = static_cast( instruction); if (destruct_instruction.variable_ == &target_variable) { state.can_be_uninitialized = true; state_modified = true; } break; } case InstructionType::Branch: case InstructionType::Dummy: case InstructionType::Return: { /* These instruction types don't change the initialization state of variables. */ break; } } if (!state_modified) { if (&instruction == entry_) { check_entry_instruction(); } for (const InstructionCursor &cursor : instruction.prev_) { if (cursor.instruction() != nullptr) { instructions_to_check.push(cursor.instruction()); } } } } return state; } class ProcedureDotExport { private: const Procedure &procedure_; dot::DirectedGraph digraph_; Map dot_nodes_by_begin_; Map dot_nodes_by_end_; public: ProcedureDotExport(const Procedure &procedure) : procedure_(procedure) { } std::string generate() { this->create_nodes(); this->create_edges(); return digraph_.to_dot_string(); } void create_nodes() { Vector all_instructions; auto add_instructions = [&](auto instructions) { all_instructions.extend(instructions.begin(), instructions.end()); }; add_instructions(procedure_.call_instructions_); add_instructions(procedure_.branch_instructions_); add_instructions(procedure_.destruct_instructions_); add_instructions(procedure_.dummy_instructions_); add_instructions(procedure_.return_instructions_); Set handled_instructions; for (const Instruction *representative : all_instructions) { if (handled_instructions.contains(representative)) { continue; } Vector block_instructions = this->get_instructions_in_block( *representative); std::stringstream ss; ss << "<"; for (const Instruction *current : block_instructions) { handled_instructions.add_new(current); switch (current->type()) { case InstructionType::Call: { this->instruction_to_string(*static_cast(current), ss); break; } case InstructionType::Destruct: { this->instruction_to_string(*static_cast(current), ss); break; } case InstructionType::Dummy: { this->instruction_to_string(*static_cast(current), ss); break; } case InstructionType::Return: { this->instruction_to_string(*static_cast(current), ss); break; } case InstructionType::Branch: { this->instruction_to_string(*static_cast(current), ss); break; } } ss << R"(
)"; } ss << ">"; dot::Node &dot_node = digraph_.new_node(ss.str()); dot_node.set_shape(dot::Attr_shape::Rectangle); dot_nodes_by_begin_.add_new(block_instructions.first(), &dot_node); dot_nodes_by_end_.add_new(block_instructions.last(), &dot_node); } } void create_edges() { auto create_edge = [&](dot::Node &from_node, const Instruction *to_instruction) -> dot::DirectedEdge & { if (to_instruction == nullptr) { dot::Node &to_node = digraph_.new_node("missing"); to_node.set_shape(dot::Attr_shape::Diamond); return digraph_.new_edge(from_node, to_node); } dot::Node &to_node = *dot_nodes_by_begin_.lookup(to_instruction); return digraph_.new_edge(from_node, to_node); }; for (auto item : dot_nodes_by_end_.items()) { const Instruction &from_instruction = *item.key; dot::Node &from_node = *item.value; switch (from_instruction.type()) { case InstructionType::Call: { const Instruction *to_instruction = static_cast(from_instruction).next(); create_edge(from_node, to_instruction); break; } case InstructionType::Destruct: { const Instruction *to_instruction = static_cast(from_instruction).next(); create_edge(from_node, to_instruction); break; } case InstructionType::Dummy: { const Instruction *to_instruction = static_cast(from_instruction).next(); create_edge(from_node, to_instruction); break; } case InstructionType::Return: { break; } case InstructionType::Branch: { const BranchInstruction &branch_instruction = static_cast( from_instruction); const Instruction *to_true_instruction = branch_instruction.branch_true(); const Instruction *to_false_instruction = branch_instruction.branch_false(); create_edge(from_node, to_true_instruction).attributes.set("color", "#118811"); create_edge(from_node, to_false_instruction).attributes.set("color", "#881111"); break; } } } dot::Node &entry_node = this->create_entry_node(); create_edge(entry_node, procedure_.entry()); } bool has_to_be_block_begin(const Instruction &instruction) { if (instruction.prev().size() != 1) { return true; } if (ELEM(instruction.prev()[0].type(), InstructionCursor::Type::Branch, InstructionCursor::Type::Entry)) { return true; } return false; } const Instruction &get_first_instruction_in_block(const Instruction &representative) { const Instruction *current = &representative; while (!this->has_to_be_block_begin(*current)) { current = current->prev()[0].instruction(); if (current == &representative) { /* There is a loop without entry or exit, just break it up here. */ break; } } return *current; } const Instruction *get_next_instruction_in_block(const Instruction &instruction, const Instruction &block_begin) { const Instruction *next = nullptr; switch (instruction.type()) { case InstructionType::Call: { next = static_cast(instruction).next(); break; } case InstructionType::Destruct: { next = static_cast(instruction).next(); break; } case InstructionType::Dummy: { next = static_cast(instruction).next(); break; } case InstructionType::Return: case InstructionType::Branch: { break; } } if (next == nullptr) { return nullptr; } if (next == &block_begin) { return nullptr; } if (this->has_to_be_block_begin(*next)) { return nullptr; } return next; } Vector get_instructions_in_block(const Instruction &representative) { Vector instructions; const Instruction &begin = this->get_first_instruction_in_block(representative); for (const Instruction *current = &begin; current != nullptr; current = this->get_next_instruction_in_block(*current, begin)) { instructions.append(current); } return instructions; } void variable_to_string(const Variable *variable, std::stringstream &ss) { if (variable == nullptr) { ss << "null"; } else { ss << "$" << variable->index_in_procedure(); if (!variable->name().is_empty()) { ss << "(" << variable->name() << ")"; } } } void instruction_name_format(StringRef name, std::stringstream &ss) { ss << name; } void instruction_to_string(const CallInstruction &instruction, std::stringstream &ss) { const MultiFunction &fn = instruction.fn(); this->instruction_name_format(fn.debug_name() + ": ", ss); for (const int param_index : fn.param_indices()) { const ParamType param_type = fn.param_type(param_index); const Variable *variable = instruction.params()[param_index]; ss << R"()"; switch (param_type.interface_type()) { case ParamType::Input: { ss << "in"; break; } case ParamType::Mutable: { ss << "mut"; break; } case ParamType::Output: { ss << "out"; break; } } ss << " "; variable_to_string(variable, ss); if (param_index < fn.param_amount() - 1) { ss << ", "; } } } void instruction_to_string(const DestructInstruction &instruction, std::stringstream &ss) { instruction_name_format("Destruct ", ss); variable_to_string(instruction.variable(), ss); } void instruction_to_string(const DummyInstruction & /*instruction*/, std::stringstream &ss) { instruction_name_format("Dummy ", ss); } void instruction_to_string(const ReturnInstruction & /*instruction*/, std::stringstream &ss) { instruction_name_format("Return ", ss); Vector outgoing_parameters; for (const ConstParameter ¶m : procedure_.params()) { if (ELEM(param.type, ParamType::Mutable, ParamType::Output)) { outgoing_parameters.append(param); } } for (const int param_index : outgoing_parameters.index_range()) { const ConstParameter ¶m = outgoing_parameters[param_index]; variable_to_string(param.variable, ss); if (param_index < outgoing_parameters.size() - 1) { ss << ", "; } } } void instruction_to_string(const BranchInstruction &instruction, std::stringstream &ss) { instruction_name_format("Branch ", ss); variable_to_string(instruction.condition(), ss); } dot::Node &create_entry_node() { std::stringstream ss; ss << "Entry: "; Vector incoming_parameters; for (const ConstParameter ¶m : procedure_.params()) { if (ELEM(param.type, ParamType::Input, ParamType::Mutable)) { incoming_parameters.append(param); } } for (const int param_index : incoming_parameters.index_range()) { const ConstParameter ¶m = incoming_parameters[param_index]; variable_to_string(param.variable, ss); if (param_index < incoming_parameters.size() - 1) { ss << ", "; } } dot::Node &node = digraph_.new_node(ss.str()); node.set_shape(dot::Attr_shape::Ellipse); return node; } }; std::string Procedure::to_dot() const { ProcedureDotExport dot_export{*this}; return dot_export.generate(); } } // namespace blender::fn::multi_function