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

Add extra checks on next tables #1287

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
37 changes: 32 additions & 5 deletions docs/JSON_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,23 +625,23 @@ attributes for these objects are:
- `actions`: the list of actions (order does not matter) supported by this
table
- `next_tables`: maps each action to a next table name. Alternatively, maps
special string `__HIT__` and `__MISS__` to a next table name.
special string `__HIT__` and `__MISS__` to a next table name. See Note 3 below.
antoninbas marked this conversation as resolved.
Show resolved Hide resolved
- `direct_meters`: the name of the associated direct meter array, or null if
the match table has no associated meter array
- `default_entry`: an optional JSON item which can force the default entry for
the table to be set when loading the JSON, without intervention from the
control plane. It has the following attributes:
- `action_id`: the id of the default action
- `action_id`: the id of the default action. Required.
- `action_const`: an optional boolean value which is `true` iff the control
plane is not allowed to change the default action function. Default value is
`false`. It can only be set to `true` for `simple` tables.
- `action_data`: an optional JSON array where each entry is the hexstring
`false`. It can only be set to `true` for `simple` tables. See Note 2 below.
- `action_data`: a required JSON array where each entry is the hexstring
value for an action argument. The size of the array needs to match the
number of parameters expected by the action function with id `action_id`.
- `action_entry_const`: an optional boolean value which is `true` iff the
control plane is not allowed to modify the action entry (action function +
action data). Default value is `false`. This attribute is ignored if the
`action_data` attribute it missing.
`action_data` attribute it missing. See Note 2 below.
- `entries`: enables you to optionally specify match-action entries for this
table. Specifying entries in the JSON makes the table immutable, which means
the added entries cannot be modified / deleted and that new entries cannot be
Expand Down Expand Up @@ -685,6 +685,33 @@ a miss every time it is applied, and execute its default action. A
dummy table has a const default action that is equal to the action `a`
in the original source code that it is replacing.

Note 2: Since May 2017 when [PR
#653](https://github.com/p4lang/p4c/pull/653) was merged into p4c, p4c
has always created tables with the value of `action_entry_const` equal
to `action_const`. They are both true if the `default_action` in the
P4 source code for the table is declared `const`, and both false if
the `default_action` is not declared `const`.
Also since 2017 and perhaps earlier, p4c has always included the key
`default_entry` in all table definitions with `type` equal to `simple`.
Since then it has _not_ included the key `default_entry` in table
definitions with types that were not `simple` (i.e. tables with
action profiles or action selectors).

Note 3: p4c always creates the value of the `next_tables` key in one
of these ways:
+ If you use the P4 constructs `t1.apply().hit` or `t1.apply().miss`,
and use that Boolean value to choose between two execution paths,
e.g. in an `if` statement, then the table's `next_tables` value will
contain the keys `__HIT__` and/or `__MISS__` to specify these two
paths, and no other keys will be present.
+ If you do not use those P4 constructs, then the `next_tables` value
will contain keys equal to the action names of the table. If the P4
program invokes the table using `switch (t1.apply().action_run)`,
then in general the different action names can specify different
next nodes to execute next, after the table is applied. If you do
not use that construct, then the next node to be executed will be
the same for all actions.

The `match_type` for the table needs to follow the following rules:
- If one match field is `range`, the table `match_type` has to be `range`
- If one match field is `ternary`, the table `match_type` has to be `ternary`
Expand Down
4 changes: 4 additions & 0 deletions include/bm/bm_sim/P4Objects.h
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ class P4Objects {
void init_meter_arrays(const Json::Value &root, InitState *);
void init_register_arrays(const Json::Value &root);
void init_actions(const Json::Value &root);
void check_next_nodes(const Json::Value &cfg_next_nodes,
const Json::Value &cfg_actions,
const std::string &table_name,
bool *next_is_hit_miss);
void init_pipelines(const Json::Value &root, LookupStructureFactory *,
InitState *);
void init_checksums(const Json::Value &root);
Expand Down
83 changes: 76 additions & 7 deletions src/bm_sim/P4Objects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,57 @@ std::vector<MatchKeyParam> parse_match_key(

} // namespace

void
P4Objects::check_next_nodes(const Json::Value &cfg_next_nodes,
const Json::Value &cfg_actions,
const std::string &table_name,
bool *next_is_hit_miss) {
// For each table, the value of its key "next_tables" must be an
// object with one of the following sets of keys:
// (a) The set of keys must include "__HIT__" and "__MISS__", but no
// others. This is how a P4 table is implemented if, where it
// is applied, it uses "t1.apply().hit" or "t1.apply().miss"
// conditions to control which code is executed next.
// (b) The set of keys must include each action name of the table
// exactly once, but no others. This is how a P4 table is
// implemented if where it is applied, it uses
// "t1.apply().action_run" and a switch statement to control
// which code is executed next. It is also used by the p4c BMv2
// backend for tables that use none of .hit, .miss, and
// .action_run, and always execute the same code next regardless
// of hit, miss, or which action the table executed. In that
// case, every action will have the same next node to execute
// regardless of the action.
int num_next_nodes = cfg_next_nodes.size();
bool next_has_hit = cfg_next_nodes.isMember("__HIT__");
bool next_has_miss = cfg_next_nodes.isMember("__MISS__");
if (next_has_hit || next_has_miss) {
if (next_has_hit && next_has_miss && (num_next_nodes == 2)) {
*next_is_hit_miss = true;
} else {
throw json_exception(
EFormat() << "Table '" << table_name << "' has one"
<< " of keys '__HIT__' and '__MISS__' in 'next_tables'"
<< " but either it does not have both of them,"
<< " or it has other keys that should not be there.",
cfg_next_nodes);
}
} else {
*next_is_hit_miss = false;
int num_actions = cfg_actions.size();
// The check that each action name is a key in cfg_next_nodes is
// done near where check_next_nodes is called, to avoid
// duplicating here the code that calculates action_name.
if (num_next_nodes != num_actions) {
throw json_exception(
EFormat() << "Table '" << table_name << "' should have exactly "
<< num_actions << " keys, one for each table action, but found "
<< num_next_nodes << "keys.",
cfg_next_nodes);
}
}
}

void
P4Objects::init_pipelines(const Json::Value &cfg_root,
LookupStructureFactory *lookup_factory,
Expand Down Expand Up @@ -1818,6 +1869,9 @@ P4Objects::init_pipelines(const Json::Value &cfg_root,
std::string actions_key = cfg_table.isMember("action_ids") ? "action_ids"
: "actions";
const Json::Value &cfg_actions = cfg_table[actions_key];
bool next_is_hit_miss = false;
check_next_nodes(cfg_next_nodes, cfg_actions, table_name,
&next_is_hit_miss);
for (const auto &cfg_action : cfg_actions) {
p4object_id_t action_id = 0;
string action_name = "";
Expand All @@ -1831,19 +1885,24 @@ P4Objects::init_pipelines(const Json::Value &cfg_root,
action = get_one_action_with_name(action_name); assert(action);
action_id = action->get_id();
}

if (!next_is_hit_miss && !cfg_next_nodes.isMember(action_name)) {
throw json_exception(
EFormat() << "Table '" << table_name << "' should have key"
<< " for action '" << action_name
<< "' in its 'next_tables' object.",
cfg_next_nodes);
}
const Json::Value &cfg_next_node = cfg_next_nodes[action_name];
const ControlFlowNode *next_node = get_next_node(cfg_next_node);
table->set_next_node(action_id, next_node);
add_action_to_table(table_name, action_name, action);
if (act_prof_name != "")
add_action_to_act_prof(act_prof_name, action_name, action);
}

if (cfg_next_nodes.isMember("__HIT__"))
table->set_next_node_hit(get_next_node(cfg_next_nodes["__HIT__"]));
if (cfg_next_nodes.isMember("__MISS__"))
table->set_next_node_miss(get_next_node(cfg_next_nodes["__MISS__"]));
if (next_is_hit_miss) {
table->set_next_node_hit(get_next_node(cfg_next_nodes["__HIT__"]));
table->set_next_node_miss(get_next_node(cfg_next_nodes["__MISS__"]));
}

if (cfg_table.isMember("base_default_next")) {
Copy link
Member

Choose a reason for hiding this comment

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

@jafingerhut do you think this is a good opportunity to mention this attribute (base_default_next) in the bmv2 JSON format document? Not sure if p4c uses it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would be happy to document it, if I could tell what it was for :-)

I see it in the JSON files generated by p4c in table definitions, pretty much most of the tables, but I also see in most tables [1] that the next_tables key defines a next node for every action name of the table. It isn't clear to me when the value of base_default_next is ever used by BMv2 code. Even if it is sometimes used, it seems to me that it might be for a feature that P4 the language does not need.

[1] the ones that don't use table.apply().hit or table.apply().miss features, which are most tables in the test suite (and typical P4 programs in general only use that feature occasionally).

Copy link
Member

Choose a reason for hiding this comment

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

So this is the original PR: #78

I think this attribute was necessary at the time to implement the reset_default_entry API.

Because in P4_16, there is always a default action with arguments (as the compiler will use NoAction if the programmer didn't provide one), I do think that this attribute is actually never used [1], unless I am missing something.
Maybe we could try to stop generating it in the p4c bmv2 backend, assuming nothing breaks (if tests break as a result, we need to come back and check what it's actually used for...). It's always good to simplify the emitted JSON. If we stop using it in p4c, we should not include it in the documentation here (discarding my initial comment), but we should keep it in the code for backwards compatibility. Maybe with a comment in P4Objects.cpp that it is no longer used by p4c.

[1] This is based on the assumption that this code is always executed for all tables when the JSON is generated by p4c:

table->set_default_default_entry(action, std::move(adata),

This assumption is only true if the default_entry attribute is set for all tables, and always includes the action_data attribute (see my other comment). Could you confirm that it is indeed true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

default_entry is not present for tables with type indirect (corresponding to implementation=action_profile) and indirect_ws (corresponding to implementation=action_selector). default_entry seems to always be present for tables with type simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I am not aware of any strong reason, other than similarity to Tofino perhaps, that we could not go ahead and implement full support for default_action in tables that have action profiles or action selectors.

Copy link
Member

Choose a reason for hiding this comment

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

It seems to me that the code is actually able to handle "regular" default actions with action data, even for indirect tables. It is actually mentioned in the PR description here: #551. It is possible that p4c was never updated in response to that to generate the proper default_entry in the JSON for such tables.

Copy link
Contributor Author

@jafingerhut jafingerhut Jan 15, 2025

Choose a reason for hiding this comment

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

Here is the code in the p4c BMv2 back end that specifically checks for, and omits, any default_action definition in the P4_16 source code for tables with action profiles or action selectors: p4lang/p4c@96ec4d5#diff-e1b9c4c9f99e44821b652694b5a72f9a58784dd8e05572850c0a08b7f7c1c87fR1215-R1221

(search for the string "Target does not support default_action" in that commit's diff)

The commit comment by Mihai was "bmv2 does not support default_action for all tables", so apparently he believed that was true at that time. For all I know, perhaps it was true for bmv2 in May 2016.

I am happy to try experimenting with a modification to p4c that removes that restriction, and test bmv2 to see if it seems to behave correctly with default_action definitions on tables with action profiles and action selectors. I agree that if bmv2 does not support them as it is written today, I suspect it would be a very small change to enable it to work for default_action's on such tables.

Copy link
Member

Choose a reason for hiding this comment

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

The commit comment by Mihai was "bmv2 does not support default_action for all tables", so apparently he believed that was true at that time. For all I know, perhaps it was true for bmv2 in May 2016.

Yes that would have been true then given that #551 is from 2018

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you are OK reviewing this PR as it is right now, I am happy to make a new issue to track the following things, perhaps modified in later PRs:
(a) consider modifying p4c bmv2 back end so that it supports default_action in P4 source code for tables with action profiles or action selectors.
(b) Document the key base_default_next, including mentioning what it was for originally, and any known cases where its value affects the behavior of bmv2.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

p4c issue to track idea (a) above: p4lang/p4c#5101
behavioral-model issue to track idea (b) above: #1289

table->set_next_node_miss_default(
Expand Down Expand Up @@ -1888,6 +1947,11 @@ P4Objects::init_pipelines(const Json::Value &cfg_root,

table->set_default_default_entry(action, std::move(adata),
is_action_entry_const);
} else {
throw json_exception(
EFormat() << "'default_entry' of table '" << table_name
<< "' should have key 'action_data'",
cfg_default_entry);
Comment on lines +1950 to +1954
Copy link
Member

Choose a reason for hiding this comment

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

see comment above. I am not sure we need to enforce this new restriction. But I don't feel very strongly about it, so if you think this is more meaningful this way, we can add it. I assume it won't break the test JSON files included in this repo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All behavioral-model CI tests are passing, as you can see on Github page for this PR.

I have also run all p4c tests with behavioral-model with the changes on this PR, and this exception never occurs.

I am OK removing the check if you prefer. The main reason I even include it is that it makes it more clear what behavioral-model can rely on going forward, and reduce the number of cases that we need to think about when modifying code related to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated docs in commit 9 to make it clearer that action_data key is required inside of default_entry.

}
}

Expand Down Expand Up @@ -1951,9 +2015,14 @@ P4Objects::init_pipelines(const Json::Value &cfg_root,
auto conditional_name = cfg_conditional["name"].asString();
auto conditional = get_conditional(conditional_name);

if (!cfg_conditional.isMember("true_next") &&
!cfg_conditional.isMember("false_next")) {
throw json_exception("conditional must have either or both of the"
" keys 'true_next' and 'false_next'.",
cfg_conditional);
}
const auto &cfg_true_next = cfg_conditional["true_next"];
const auto &cfg_false_next = cfg_conditional["false_next"];

if (!cfg_true_next.isNull()) {
auto next_node = get_control_node_cfg(cfg_true_next.asString());
conditional->set_next_node_if_true(next_node);
Expand Down
Loading