blob: 50d4ce36bc1b033b51ed816991009b9851a3b76c [file]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "mpact/sim/decoder/proto_instruction_encoding.h"
#include <algorithm>
#include <functional>
#include <string>
#include <vector>
#include "absl/container/btree_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/string_view.h"
#include "mpact/sim/decoder/format_name.h"
#include "mpact/sim/decoder/proto_constraint_expression.h"
#include "mpact/sim/decoder/proto_format_contexts.h"
#include "src/google/protobuf/descriptor.h"
namespace mpact {
namespace sim {
namespace decoder {
namespace proto_fmt {
using mpact::sim::machine_description::instruction_set::ToPascalCase;
ProtoInstructionEncoding::ProtoInstructionEncoding(
std::string name, ProtoInstructionGroup *parent)
: name_(name), instruction_group_(parent) {}
// Copy constructor.
ProtoInstructionEncoding::ProtoInstructionEncoding(
const ProtoInstructionEncoding &rhs) {
name_ = rhs.name_;
instruction_group_ = rhs.instruction_group_;
setter_code_ = rhs.setter_code_;
for (const auto &[name, setter_ptr] : rhs.setter_map_) {
setter_map_.insert({name, new ProtoSetter(*setter_ptr)});
}
for (const auto *constraint : rhs.equal_constraints_) {
equal_constraints_.push_back(new ProtoConstraint(*constraint));
}
for (const auto *constraint : rhs.other_constraints_) {
other_constraints_.push_back(new ProtoConstraint(*constraint));
}
for (const auto &[name, constraint_ptr] : rhs.has_constraints_) {
has_constraints_.insert({name, new ProtoConstraint(*constraint_ptr)});
}
}
ProtoInstructionEncoding::~ProtoInstructionEncoding() {
for (const auto *constraint : equal_constraints_) {
delete constraint->expr;
delete constraint;
}
equal_constraints_.clear();
for (const auto *constraint : other_constraints_) {
delete constraint->expr;
delete constraint;
}
other_constraints_.clear();
for (const auto &[unused, constraint] : has_constraints_) {
delete constraint;
}
has_constraints_.clear();
for (const auto &[unused, setter] : setter_map_) {
delete setter;
}
setter_map_.clear();
}
absl::Status ProtoInstructionEncoding::AddSetter(
SetterDefCtx *ctx, const std::string &name,
const google::protobuf::FieldDescriptor *field_descriptor,
const std::vector<const google::protobuf::FieldDescriptor *> &one_of_fields,
IfNotCtx *if_not) {
if (ctx == nullptr) return absl::InvalidArgumentError("Context is null");
// If there is a setter already for that name, return an error.
auto iter = setter_map_.find(name);
if (iter != setter_map_.end()) {
return absl::AlreadyExistsError(
absl::StrCat("Setter '", name, "' already defined."));
}
// Setters are added after constraints. For each depends_on, see if the
// constraint already exists for the encoding. If so, remove it from the
// one_of_fields vector, as it is guaranteed to be satisfied if the
// instruction is successfully decoded. If it contradicts an existing
// constraint, signal an error.
ProtoConstraint *depends_on = nullptr;
for (auto const *desc : one_of_fields) {
auto iter = oneof_field_map_.find(desc->containing_oneof());
if (iter != oneof_field_map_.end()) {
// Duplicate of an encoding constraint.
if (iter->second == desc) continue;
// Conflict.
return absl::InternalError(absl::StrCat(
"One_of constraint on '", desc->name(),
"' contradicts encoding constraint on '", iter->second->name(), "'"));
}
depends_on = AddHasConstraint(desc, depends_on);
}
// Add the setter information.
setter_map_.emplace(
name, new ProtoSetter{ctx, name, field_descriptor, if_not, depends_on});
return absl::OkStatus();
}
absl::Status ProtoInstructionEncoding::AddConstraint(
FieldConstraintCtx *ctx, ConstraintType op,
const google::protobuf::FieldDescriptor *field_descriptor,
const std::vector<const google::protobuf::FieldDescriptor *> &one_of_fields,
const ProtoConstraintExpression *expr) {
// One_of_fields is a list of fields that have 'kHas' constraints that are
// prerequisites for the constraint being added. The variable depends_on
// points to the end of the dependence chain, or nullptr if there are no or
// duplicate one_of field constraints.
ProtoConstraint *depends_on = nullptr;
for (auto const *desc : one_of_fields) {
auto iter = oneof_field_map_.find(desc->containing_oneof());
if (iter != oneof_field_map_.end()) {
if (iter->second == field_descriptor) {
continue; // Ignore duplicate.
} else {
// This contradicts a previous one_of constraint. Flag an error.
return absl::InternalError(
absl::StrCat("One_of constraint on '", desc->name(),
"' contradicts previous constraint on '",
iter->second->name(), "'"));
}
}
// Add the one_of to the oneof_field_map_.
oneof_field_map_.emplace(desc->containing_oneof(), desc);
depends_on = AddHasConstraint(desc, depends_on);
}
// In order to generate a reasonably efficient decoder we divide the
// constraints into two sets, those that can be used as indices into function
// call tables or used as values in switch statements to differentiate between
// the most instructions, and those that have to be evaluated in a slower
// (often serial) manner. Only constraints on fields that don't depend on
// other one_of fields can be treated in this manner. A dependency on one_of
// fields can be used to create additional constraints, of which one is at the
// top level in the proto.
if (!one_of_fields.empty()) {
// Add equal constraint on the first one_of_field dependency.
equal_constraints_.push_back(new ProtoConstraint{
ctx, one_of_fields[0], ConstraintType::kHas, nullptr, 0, nullptr});
// Add the remaining one_of depende ncies to the other constraints.
for (int i = 1; i < one_of_fields.size(); ++i) {
other_constraints_.push_back(new ProtoConstraint{
ctx, one_of_fields[i], ConstraintType::kHas, nullptr, 0, nullptr});
// Add the constraint to the 'other' constraints.
other_constraints_.push_back(
new ProtoConstraint{ctx, field_descriptor, op, expr, 0, depends_on});
}
return absl::OkStatus();
}
// A kHas constraint on a member of a one_of field is equivalent to an kEq
// constraint on the value of the one_of '_value()' function, so add it to the
// equal_constraints vector. For kEq constraints, if the type of the field is
// not an integer type, put it in the other_constraints.
if ((op == ConstraintType::kEq) &&
(IsIntType(kCppToVariantTypeMap[field_descriptor->cpp_type()]))) {
// If it's an equal constraint with an integer type, add it to the 'equal'
// constraints.
equal_constraints_.push_back(
new ProtoConstraint{ctx, field_descriptor, op, expr, 0, nullptr});
} else if ((op == ConstraintType::kHas) &&
(field_descriptor->containing_oneof() != nullptr)) {
// If it's a kHas constraint on an one_of field, first make sure that it
// does not contradict or duplicate any previous one_of kHas constraints.
auto *oneof_desc = field_descriptor->containing_oneof();
auto iter = oneof_field_map_.find(oneof_desc);
if (iter != oneof_field_map_.end()) {
// There is already a constraint on this oneof. Either it's the same
// constraint, which means we can ignore this one, or it is for a
// different field in which case it is a contradiction. Either way, the
// constraint does not get added.
if (iter->second == field_descriptor) {
// It is a duplicate constraint. just ignore.
return absl::OkStatus();
}
// It is a different field of the one_of, so there is a contradiction.
// Return an error.
return absl::InternalError(absl::StrCat(
"One_of constraint on '", field_descriptor->name(),
"' contradicts previous constraint on '", iter->second->name(), "'"));
}
oneof_field_map_.emplace(oneof_desc, field_descriptor);
equal_constraints_.push_back(
new ProtoConstraint{ctx, field_descriptor, op, nullptr, 0, nullptr});
} else {
// Otherwise add it to the 'other' constraints.
other_constraints_.push_back(
new ProtoConstraint{ctx, field_descriptor, op, expr, 0, nullptr});
}
return absl::OkStatus();
}
ProtoConstraint *ProtoInstructionEncoding::AddHasConstraint(
const google::protobuf::FieldDescriptor *field_descriptor,
ProtoConstraint *depends_on) {
if (depends_on != nullptr) {
if (!has_constraints_.contains(depends_on->field_descriptor->full_name())) {
return nullptr;
}
}
auto iter = has_constraints_.find(field_descriptor->full_name());
if (iter != has_constraints_.end()) return iter->second;
ProtoConstraint *constraint = new ProtoConstraint{
nullptr, field_descriptor, ConstraintType::kHas, nullptr, 0, depends_on};
has_constraints_.insert({field_descriptor->full_name(), constraint});
return constraint;
}
// This function returns a string with properly indented C++ code for the
// setters for this instruction. The '$' is used instead of the message name.
void ProtoInstructionEncoding::GenerateSetterCode() {
const int kIndent = 0;
if (setter_map_.empty()) return;
absl::StrAppend(&setter_code_, "/* setters for ", name(), " */\n");
// First need to group setters by dependencies on fields, split into setters
// with if_not and those without (except for those with no depends_on.).
// Also need to group constraints by their dependencies. Use multimap that
// maps from a constraint to those that depend on it.
absl::btree_multimap<const ProtoConstraint *, const ProtoConstraint *>
grouped_constraints;
// Maintain a set of inserted constraints, so that the multimap has no
// duplicate key-value pairs.
absl::flat_hash_set<const ProtoConstraint *> inserted_constraints;
// This set contains the top level constraints that do not depend on any
// other constraints, and thus are the beginning of the 'dependence chains'.
absl::flat_hash_set<const ProtoConstraint *> constraint_tops;
// These multimaps map from a constraint to the set of setters dependent on
// that constraint.
absl::btree_multimap<const ProtoConstraint *, const ProtoSetter *>
grouped_setters;
absl::btree_multimap<const ProtoConstraint *, const ProtoSetter *>
grouped_if_not_setters;
// Lambda used to determine if a constraint is already satisfied by
// an identical constraint used in the decoding of the instruction.
auto is_in_eq_constraints = [&](const ProtoConstraint *constraint) {
auto *field_descriptor = constraint->field_descriptor;
auto iter = std::find_if(
equal_constraints_.begin(), equal_constraints_.end(),
[&field_descriptor](const ProtoConstraint *constraint) {
return (constraint->op == ConstraintType::kHas) &&
(constraint->field_descriptor == field_descriptor);
});
return (iter != equal_constraints_.end());
};
// First build up the data structures.
// Iterate over the setters for this instruction.
for (auto &[name, setter_ptr] : setter_map_) {
// Get any one_of dependency that the setter depends on.
auto *depends = setter_ptr->depends_on;
// If the dependency matches one in the equal constraints for decoding the
// instruction, it will be true for the setters, and does not have to be
// tested for again.
if (depends != nullptr && is_in_eq_constraints(depends)) depends = nullptr;
// Group the setter in a multimap that maps from a constraint to a
// dependent constraint. There are two sets, depending on whether the setter
// has an if_not clause or not. Setters with null dependency are inserted in
// the regular group regardless of if_not value.
if (depends != nullptr && setter_ptr->if_not != nullptr) {
grouped_if_not_setters.emplace(depends, setter_ptr);
} else {
grouped_setters.emplace(depends, setter_ptr);
}
// If there is no one_of dependency, or the setter has an if_not,
// go to the next setter.
if (depends == nullptr) continue;
if (setter_ptr->if_not != nullptr) continue;
// Add dependency 'links' to the grouped_constraints map, that map
// from a constraint to the constraints that depend on it.
while (depends != nullptr && depends->depends_on != nullptr) {
// See if 'depends' has been inserted yet, if not, add a map entry from
// the one_of it depends on to it in the grouped_constraints multi map.
auto [unused, inserted] = inserted_constraints.insert(depends);
if (inserted) {
grouped_constraints.emplace(depends->depends_on, depends);
}
// Go to the next dependency in the chain, but make sure to skip any
// that are satisfied by the equal constraints.
depends = depends->depends_on;
}
// Maintain a set of the the top level constraints (which do not depend
// on other constraints).
constraint_tops.insert(depends);
}
// Helper lambda functions used in the loop nest below.
// This generates the assignment.
auto assign = [&](int indent, const ProtoSetter *setter) {
absl::StrAppend(&setter_code_, std::string(indent, ' '), "decoder->Set",
ToPascalCase(setter->name), "($.");
auto field_name = setter->ctx->qualified_ident()->getText();
// Need to convert from a.b.c to a().b().c().
auto call =
absl::StrCat(absl::StrReplaceAll(field_name, {{".", "()."}}), "()");
if (setter->if_not == nullptr) {
absl::StrAppend(&setter_code_, call, "); // ",
setter->field_descriptor->full_name(), "\n");
} else {
auto pos = call.find_last_of('.');
std::string name;
if (pos != std::string::npos) {
std::string prefix = call.substr(0, pos + 1);
name = prefix + "has_" + call.substr(pos + 1);
} else {
name = "has_" + call;
}
std::string txt = setter->if_not->value() != nullptr
? setter->if_not->value()->getText()
: setter->if_not->qualified_ident()->getText();
absl::StrAppend(&setter_code_, name, " ? $.", call, " : ", txt, ");\n");
}
};
// First process the setters with no oneof dependencies.
auto [begin, end] = grouped_setters.equal_range(nullptr);
for (auto iter = begin; iter != end; ++iter) assign(kIndent, iter->second);
// Helper lambda function to generate the if statement to guard individual
// setters.
auto generate_if_statement = [&](int indent,
const ProtoConstraint *constraint) {
auto *desc = constraint->field_descriptor;
auto *oneof = desc->containing_oneof();
absl::StrAppend(&setter_code_, std::string(indent, ' '), "if ($.");
if (oneof != nullptr) {
absl::StrAppend(&setter_code_, oneof->name(),
"_case() == ", ToPascalCase(oneof->name()), "Case::k",
ToPascalCase(desc->name()), ") {\n");
} else {
absl::StrAppend(&setter_code_, "has_", desc->name(), ") {\n");
}
};
// Recursive lambda for generating nested if statements around groups of
// setters with the same constraint.
std::function<void(int, const ProtoConstraint *)> generate_nested_ifs =
[&](int indent, const ProtoConstraint *constraint) {
// Generate if statement for 'constraint'.
generate_if_statement(indent, constraint);
indent += 2;
// Perform all the assigns that depend on constraint.
{
auto [begin, end] = grouped_setters.equal_range(constraint);
for (auto iter = begin; iter != end; ++iter)
assign(indent, iter->second);
}
// Generate any ifs for constraints dependent on the current constraint.
{
auto [begin, end] = grouped_constraints.equal_range(constraint);
for (auto iter = begin; iter != end; ++iter) {
generate_nested_ifs(indent, iter->second);
}
}
indent -= 2;
absl::StrAppend(&setter_code_, std::string(indent, ' '), "}\n");
};
// Process the setters with no if_not's.
for (auto *constraint : constraint_tops) {
generate_nested_ifs(kIndent, constraint);
}
// Recursive lambda for generating the conditions of the if statements used
// by setters with 'if_not' constructs.
std::function<void(const ProtoConstraint *, std::string &)>
recursive_if_conditions =
[&](const ProtoConstraint *constraint, std::string &if_conditions) {
auto *desc = constraint->field_descriptor;
auto *depends_on = constraint->depends_on;
std::string sep = "";
// Generate the conditions in reverse order of the depends_on list.
if (depends_on != nullptr) {
recursive_if_conditions(depends_on, if_conditions);
if (!if_conditions.empty()) sep = " && ";
}
auto *oneof = desc->containing_oneof();
std::string ident = constraint->ctx->qualified_ident()->getText();
auto pos = ident.find_last_of('.');
std::string prefix;
if (pos != std::string::npos) {
prefix = ident.substr(0, pos + 1);
}
if (!prefix.empty()) {
// Convert a.b.c to a().b().c().
prefix = absl::StrReplaceAll(prefix, {{".", "()."}});
}
if (oneof != nullptr) {
absl::StrAppend(&if_conditions, sep, "($.", prefix, oneof->name(),
"_case() == k", ToPascalCase(desc->name()), ")");
} else {
absl::StrAppend(&if_conditions, sep, "($.", prefix, "has_",
desc->name(), ")");
}
};
// Process the setters with dependencies and if_not's.
for (auto iter = grouped_if_not_setters.begin();
iter != grouped_if_not_setters.end();
/*empty increment expression - it's done inside nested loop below*/) {
std::string if_conditions;
recursive_if_conditions(iter->first, if_conditions);
absl::StrAppend(&setter_code_, std::string(kIndent, ' '), "if (",
if_conditions, ") {\n");
auto count = grouped_if_not_setters.count(iter->first);
for (auto i = 0; i < count; ++i) {
assign(kIndent + 2, iter->second);
++iter;
}
absl::StrAppend(&setter_code_, std::string(kIndent, ' '), "}\n");
}
}
std::string ProtoInstructionEncoding::GetSetterCode(
absl::string_view message_name, int indent) const {
return absl::StrReplaceAll(
setter_code_,
{{"$", message_name}, {"\n", "\n" + std::string(indent, ' ')}});
}
} // namespace proto_fmt
} // namespace decoder
} // namespace sim
} // namespace mpact