Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

P4Tools: Implement default action override for BMv2 STF. #3685

Merged
merged 4 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug in the STF test framework. Default action overrides were not using the correct syntax.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

}

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all test frameworks support ActionProfiles and ActionSelectors - do we want to move it to common tf?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is shared between enough back ends that we can add it to common.

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