Skip to content

Commit

Permalink
P4Tools: Implement default action override for BMv2 STF. (#3685)
Browse files Browse the repository at this point in the history
  • Loading branch information
fruffy authored Nov 22, 2022
1 parent 8020c1d commit 95e7894
Show file tree
Hide file tree
Showing 19 changed files with 396 additions and 331 deletions.
2 changes: 1 addition & 1 deletion backends/bmv2/bmv2stf.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ def parse_table_set_default(self, cmd):
actionArgs.set(k, v)
command = "table_set_default " + tableName + " " + actionName
if actionArgs.size():
command += " => " + str(actionArgs)
command += " " + str(actionArgs)
return command
def parse_table_add(self, cmd):
tableName, cmd = nextWord(cmd)
Expand Down
102 changes: 80 additions & 22 deletions backends/p4tools/testgen/core/small_step/table_stepper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ bool TableStepper::compareLPMEntries(const IR::Entry* leftIn, const IR::Entry* r
if (const auto* rightMask = right->checkedTo<IR::Mask>()) {
rightMaskVal = rightMask->right->checkedTo<IR::Constant>();
}
if ((leftMaskVal->value) < (rightMaskVal->value)) {
return false;
}
return true;
return (leftMaskVal->value) >= (rightMaskVal->value);
}

BUG("Unhandled sort elements type: left: %1% - %2% \n right: %3% - %4% ", left,
Expand Down Expand Up @@ -167,10 +164,8 @@ const IR::Expression* TableStepper::computeTargetMatchType(
TESTGEN_UNIMPLEMENTED("Match type %s not implemented for table keys.", keyProperties.matchType);
}

const IR::Expression* TableStepper::computeHit(ExecutionState* nextState, const IR::P4Table* table,
const IR::Expression* TableStepper::computeHit(ExecutionState* nextState,
std::map<cstring, const FieldMatch>* matches) {
const auto* key = table->getKey();
CHECK_NULL(key);
const IR::Expression* hitCondition = IR::getBoolLiteral(!properties.resolvedKeys.empty());
for (auto keyProperties : properties.resolvedKeys) {
hitCondition = computeTargetMatchType(nextState, keyProperties, matches, hitCondition);
Expand Down Expand Up @@ -209,10 +204,7 @@ const IR::Expression* TableStepper::evalTableConstEntries() {
const IR::Expression* tableMissCondition = IR::getBoolLiteral(true);

const auto* keys = table->getKey();
if (keys == nullptr) {
// No keys to match on. Assume just the default action will be executed.
return tableMissCondition;
}
BUG_CHECK(keys != nullptr, "An empty key list should have been handled earlier.");

const auto* entries = table->getEntries();
// Sometimes, there are no entries. Just return.
Expand Down Expand Up @@ -318,13 +310,75 @@ const IR::Expression* TableStepper::evalTableConstEntries() {
return tableMissCondition;
}

void TableStepper::setTableDefaultEntries(
const std::vector<const IR::ActionListElement*>& tableActionList) {
for (size_t idx = 0; idx < tableActionList.size(); idx++) {
const auto* action = tableActionList.at(idx);
// Grab the path from the method call.
const auto* tableAction = action->expression->checkedTo<IR::MethodCallExpression>();
// Try to find the action declaration corresponding to the path reference in the table.
const auto* actionType = stepper->state.getActionDecl(tableAction->method);
CHECK_NULL(actionType);

auto* nextState = new ExecutionState(stepper->state);

// We get the control plane name of the action we are calling.
cstring actionName = actionType->controlPlaneName();
// Synthesize arguments for the call based on the action parameters.
const auto& parameters = actionType->parameters;
auto* arguments = new IR::Vector<IR::Argument>();
std::vector<ActionArg> ctrlPlaneArgs;
for (size_t argIdx = 0; argIdx < parameters->size(); ++argIdx) {
const auto* parameter = parameters->getParameter(argIdx);
// Synthesize a zombie constant here that corresponds to a control plane argument.
// We get the unique name of the table coupled with the unique name of the action.
// Getting the unique name is needed to avoid generating duplicate arguments.
const auto& actionDataVar =
Utils::getZombieTableVar(parameter->type, table, "*actionData", idx, argIdx);
cstring paramName =
properties.tableName + "_arg_" + actionName + std::to_string(argIdx);
const auto& actionArg = nextState->createZombieConst(parameter->type, paramName);
nextState->set(actionDataVar, actionArg);
arguments->push_back(new IR::Argument(actionArg));
// We also track the argument we synthesize for the control plane.
// Note how we use the control plane name for the parameter here.
ctrlPlaneArgs.emplace_back(parameter, actionArg);
}
const auto* ctrlPlaneActionCall = new ActionCall(actionType, ctrlPlaneArgs);

// We add the arguments to our action call, effectively creating a const entry call.
auto* synthesizedAction = tableAction->clone();
synthesizedAction->arguments = arguments;

// We need to set the table action in the state for eventual switch action_run hits.
// We also will need it for control plane table entries.
setTableAction(nextState, tableAction);

// Finally, add all the new rules to the execution stepper->state.
auto* tableConfig = new TableConfig(table, {});
// Add the action selector to the table. This signifies a slightly different implementation.
tableConfig->addTableProperty("overriden_default_action", ctrlPlaneActionCall);
nextState->addTestObject("tableconfigs", properties.tableName, tableConfig);

// Update all the tracking variables for tables.
std::vector<Continuation::Command> replacements;
replacements.emplace_back(new IR::MethodCallStatement(synthesizedAction));

nextState->set(getTableHitVar(table), IR::getBoolLiteral(false));
nextState->set(getTableReachedVar(table), IR::getBoolLiteral(true));
std::stringstream tableStream;
tableStream << "Table Branch: " << properties.tableName;
tableStream << "| Overriding default action: " << actionName;
nextState->add(new TraceEvent::Generic(tableStream.str()));
nextState->replaceTopBody(&replacements);
stepper->result->emplace_back(nextState);
}
}

void TableStepper::evalTableControlEntries(
const std::vector<const IR::ActionListElement*>& tableActionList) {
const auto* keys = table->getKey();
// If we have no keys, there is nothing to match.
if (keys == nullptr) {
return;
}
BUG_CHECK(keys != nullptr, "An empty key list should have been handled earlier.");

for (size_t idx = 0; idx < tableActionList.size(); idx++) {
const auto* action = tableActionList.at(idx);
Expand All @@ -338,7 +392,7 @@ void TableStepper::evalTableControlEntries(

// First, we compute the hit condition to trigger this particular action call.
std::map<cstring, const FieldMatch> matches;
const auto* hitCondition = computeHit(nextState, table, &matches);
const auto* hitCondition = computeHit(nextState, &matches);

// We get the control plane name of the action we are calling.
cstring actionName = actionType->controlPlaneName();
Expand Down Expand Up @@ -518,14 +572,18 @@ bool TableStepper::resolveTableKeys() {
return false;
}

bool TableStepper::checkTableIsImmutable() {
void TableStepper::checkTableIsImmutable() {
bool isConstant = false;
if (table->properties->getProperty("entries") != nullptr) {
isConstant = table->properties->getProperty("entries")->isConstant;
const auto* entriesAnnotation = table->properties->getProperty("entries");
if (entriesAnnotation != nullptr) {
isConstant = entriesAnnotation->isConstant;
}
// Also check if the table is invisible to the control plane.
// This also implies that it cannot be modified.
return isConstant || table->getAnnotation("hidden") != nullptr;
properties.tableIsImmutable = isConstant || table->getAnnotation("hidden") != nullptr;
const auto* defaultAction = table->properties->getProperty("default_action");
CHECK_NULL(defaultAction);
properties.defaultIsImmutable = defaultAction->isConstant;
}

std::vector<const IR::ActionListElement*> TableStepper::buildTableActionList() {
Expand Down Expand Up @@ -595,8 +653,8 @@ void TableStepper::evalTargetTable(
}

bool TableStepper::eval() {
// Check whether the table is immutable, meaning it has constant entries.
properties.tableIsImmutable = checkTableIsImmutable();
// Set the appropriate properties when the table is immutable, meaning it has constant entries.
checkTableIsImmutable();
// Resolve any non-symbolic table keys. The function returns true when a key needs replacement.
if (resolveTableKeys()) {
return false;
Expand Down
9 changes: 7 additions & 2 deletions backends/p4tools/testgen/core/small_step/table_stepper.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class TableStepper {
/// Whether the table is constant and can not accept control plane entries.
bool tableIsImmutable = false;

/// Indicates whether the default action can be overridden.
bool defaultIsImmutable = false;

/// Ordered list of key fields with useful properties.
std::vector<KeyProperties> resolvedKeys;
} properties;
Expand Down Expand Up @@ -112,7 +115,7 @@ class TableStepper {
size_t lpmIndex);

/// @returns whether the table is immutable, meaning control plane entries can not be added.
bool checkTableIsImmutable();
void checkTableIsImmutable();

/// Add the default action path to the expression stepper. If only hits if @param
/// tableMissCondition is true.
Expand Down Expand Up @@ -143,7 +146,7 @@ class TableStepper {
/// handle constant entries, it is specialized for control plane entries.
/// The function also tracks the list of field matches created to achieve a hit. We later use
/// this to insert table entries using the STF/PTF framework.
const IR::Expression* computeHit(ExecutionState* nextState, const IR::P4Table* table,
const IR::Expression* computeHit(ExecutionState* nextState,
std::map<cstring, const FieldMatch>* matches);

/// Collects properties that may be set per table. Target back end may have different semantics
Expand Down Expand Up @@ -186,6 +189,8 @@ class TableStepper {
/// execution. How a table is evaluated is target-specific.
virtual void evalTargetTable(const std::vector<const IR::ActionListElement*>& tableActionList);

void setTableDefaultEntries(const std::vector<const IR::ActionListElement*>& tableActionList);

public:
/// Table implementations in P4 are rather flexible. Eval is a delegation function that chooses
/// the right implementation depending on the properties of the table. For example, immutable
Expand Down
102 changes: 101 additions & 1 deletion backends/p4tools/testgen/lib/tf.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#include <cstddef>

#include <boost/optional/optional.hpp>
#include <inja/inja.hpp>

#include "backends/p4tools/common/lib/format_int.h"
#include "lib/cstring.h"

#include "backends/p4tools/testgen/lib/test_spec.h"
Expand All @@ -14,7 +16,7 @@ namespace P4Tools {
namespace P4Testgen {

/// THe default base class for the various test frameworks (TF). Every test framework has a test
/// name and a seed associated with it.
/// name and a seed associated with it. Also contains a variety of common utility functions.
class TF {
protected:
/// The @testName to be used in test case generation.
Expand All @@ -26,6 +28,104 @@ class TF {
/// Creates a generic test framework.
explicit TF(cstring, boost::optional<unsigned int>);

/// Converts the traces of this test into a string representation and Inja object.
static inja::json getTrace(const TestSpec* testSpec) {
inja::json traceList = inja::json::array();
const auto* traces = testSpec->getTraces();
if ((traces != nullptr) && !traces->empty()) {
for (const auto& trace : *traces) {
std::stringstream ss;
ss << *trace;
traceList.push_back(ss.str());
}
}
return traceList;
}

/// Checks whether a table object has an action profile or selector associated with it.
/// If that is the case, we set a boolean flag for this particular inja object.
template <class ProfileType, class SelectorType>
static void checkForTableActionProfile(inja::json& tblJson, std::map<cstring, cstring>& apAsMap,
const TableConfig* tblConfig) {
const auto* apObject = tblConfig->getProperty("action_profile", false);
if (apObject != nullptr) {
const auto* actionProfile = apObject->checkedTo<ProfileType>();
tblJson["has_ap"] = true;
// Check if we have an Action Selector too.
// TODO: Change this to check in ActionSelector with table
// property "action_selectors".
const auto* asObject = tblConfig->getProperty("action_selector", false);
if (asObject != nullptr) {
const auto* actionSelector = asObject->checkedTo<SelectorType>();
apAsMap[actionProfile->getProfileDecl()->controlPlaneName()] =
actionSelector->getSelectorDecl()->controlPlaneName();
tblJson["has_as"] = true;
}
}
}

/// Check whether the table object has an overridden default action.
/// In this case, we assume there are no keys and we just set the default action of the table.
static void checkForDefaultActionOverride(inja::json& tblJson, const TableConfig* tblConfig) {
const auto* defaultOverrideObj = tblConfig->getProperty("overriden_default_action", false);
if (defaultOverrideObj != nullptr) {
const auto* defaultAction = defaultOverrideObj->checkedTo<ActionCall>();
inja::json a;
a["action_name"] = defaultAction->getActionName();
auto const* actionArgs = defaultAction->getArgs();
inja::json b = inja::json::array();
for (const auto& actArg : *actionArgs) {
inja::json j;
j["param"] = actArg.getActionParamName().c_str();
j["value"] = formatHexExpr(actArg.getEvaluatedValue());
b.push_back(j);
}
a["act_args"] = b;
tblJson["default_override"] = a;
}
}

/// Collect all the action profile objects. These will have to be declared in the test.
template <class ProfileType>
static void collectActionProfileDeclarations(const TestSpec* testSpec,
inja::json& controlPlaneJson,
const std::map<cstring, cstring>& apAsMap) {
auto actionProfiles = testSpec->getTestObjectCategory("action_profiles");
if (!actionProfiles.empty()) {
controlPlaneJson["action_profiles"] = inja::json::array();
}
for (auto const& testObject : actionProfiles) {
const auto* const actionProfile = testObject.second->checkedTo<ProfileType>();
const auto* actions = actionProfile->getActions();
inja::json j;
j["profile"] = actionProfile->getProfileDecl()->controlPlaneName();
j["actions"] = inja::json::array();
for (size_t idx = 0; idx < actions->size(); ++idx) {
const auto& action = actions->at(idx);
auto actionName = action.first;
auto actionArgs = action.second;
inja::json a;
a["action_name"] = actionName;
a["action_idx"] = std::to_string(idx);
inja::json b = inja::json::array();
for (const auto& actArg : actionArgs) {
inja::json c;
c["param"] = actArg.getActionParamName().c_str();
c["value"] = formatHexExpr(actArg.getEvaluatedValue()).c_str();
b.push_back(c);
}
a["act_args"] = b;
j["actions"].push_back(a);
}
// Look up the selectors associated with the profile.
if (apAsMap.find(actionProfile->getProfileDecl()->controlPlaneName()) !=
apAsMap.end()) {
j["selector"] = apAsMap.at(actionProfile->getProfileDecl()->controlPlaneName());
}
controlPlaneJson["action_profiles"].push_back(j);
}
}

public:
/// The method used to output the test case to be implemented by
/// all the test frameworks (eg. STF, PTF, etc.).
Expand Down
Loading

0 comments on commit 95e7894

Please sign in to comment.