p4lang / p4c

P4_16 reference compiler

Home Page:https://p4.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`default` expression is accepted in select case expressions with multiple components

kfcripps opened this issue · comments

The following P4 program:

#include <core.p4>

typedef bit b1;

enum b1 e
{
  value = 1
}
typedef bool t;

struct s
{
  t f1;
  t f2;
}

header h_t {
    s b;
    bool x;
    bool y;

    bit<8> f; bit<8> g;
}

// No error message about (default, false) switch case label
parser p(packet_in packet) {
    state start {
        h_t h;
        packet.extract(h);
        transition select(h.x, h.y) {
                (true, false): state1;
                (default, false): accept;
        }
    }
    state state1 {
        transition accept;
    }
}

parser simple(packet_in packet);
package top(simple e);
top(p()) main;

compiles successfully, which seems odd, but I'm not sure what the expected behavior is per the spec.

I may be missing some subtle thing that makes this program's behavior not defined according to the spec, so feel free to point out any aspect of the below that seems wrong:

  • packet.extract(h) will extract 2+1+1+8+8=20 bits from the packet into h, with values of 0 being converted to bool value false, and 1 to true.
  • If h.x is true and h.y is false, transition to state1
  • otherwise, if h.y is false, transition to accept

What am I missing?

Here is a small test program that runs on BMv2, based on your code but with small changes to enable it to compile and run on BMv2 (e.g. make header type h_t a multiple of 8 bits long), how to compile it and see the I-believe-equivalent P4_16 program that the BMv2 back end produces, and how to generate and run 5 test cases that exercise all conditional paths through the parser and ingress control that BMv2 executes correctly:

@jafingerhut My point is that it seems like an oversight in the spec (I don't see any mention of this in either direction) and/or in the compiler that e.g. (default, false) is a valid switch case label. Note that the following is also accepted by the compiler, for example:

...
        transition select(h.x, h.y) {
                (default, default): state1;
                (default, default): accept;
        }
...

which makes me think that some switch case validation is missing from the compiler.

Also, I wonder what would be generated for:

...
        transition select(h.f, h.g) {
                (default, default): state1;
                (8w1, 8w2): accept;
        }
...

for the BMV2 backend.

If you are saying that a switch case label of (default, default) is equivalent to a switch case label of default, then I would expect the second switch case to be unreachable. I don't think the compiler considers these two labels to be equivalent. I believe it only treats default specially.

At least according to the specification this seems valid to me?
https://p4.org/p4-spec/docs/P4-16-working-spec.html#sec-set-exprs

A default is equivalent to _. This duplication appears like an oversight to me, however.

If you want to guarantee in the compiler that you always detect select branches that are unmatchable, in general you will need a satisfiability solver to do it. You can get it with Z3 off the shelf, of course, and if someone wants to implement that, be my guest.

The compiler does treat (default, default) the same as default, as far as I can tell from simple experiments. If you have evidence of a bug in this area, definitely good to have a test program that exhibits it.

There was a pass in the works to remove or simplify overlapping matches but it was never finished:
#2591

The compiler does treat (default, default) the same as default, as far as I can tell from simple experiments. If you have evidence of a bug in this area, definitely good to have a test program that exhibits it.

@jafingerhut Sure, below are two examples which exhibit different behavior when using default vs (default, default). The observable difference (a warning) is benign in this case, but makes me believe that there may be other observable differences (either with this case, or other cases) that are less benign.

The following P4 program:

#include <core.p4>

typedef bit b1;

enum b1 e
{
  value = 1
}
typedef bool t;

struct s
{
  t f1;
  t f2;
}

header h_t {
    s b;
    bool x;
    bool y;

    bit<8> f; bit<8> g;
}

parser p(packet_in packet) {
    state start {
        h_t h;
        packet.extract(h);
        bit<8> w = h.minSizeInBits();
        transition select(h.f, w) {
                default: state1;
                (8w1, 8w2): accept;
        }
    }
    state state1 {
        transition accept;
    }
}

parser simple(packet_in packet);
package top(simple e);
top(p()) main;

produces the following warnings:

ex1.p4(32): [--Wwarn=parser-transition] warning: SelectCase: unreachable
                (8w1, 8w2): accept;
                ^^^^^^^^^^^^^^^^^^
ex1.p4(30): [--Wwarn=parser-transition] warning: '{ h.f, w }': transition does not depend on select argument
        transition select(h.f, w) {
                          ^^^^^^

and the following program produces no warnings or errors:

#include <core.p4>

typedef bit b1;

enum b1 e
{
  value = 1
}
typedef bool t;

struct s
{
  t f1;
  t f2;
}

header h_t {
    s b;
    bool x;
    bool y;

    bit<8> f; bit<8> g;
}

parser p(packet_in packet) {
    state start {
        h_t h;
        packet.extract(h);
        bit<8> w = h.minSizeInBits();
        transition select(h.f, w) {
                (default, default): state1;
                (8w1, 8w2): accept;
        }
    }
    state state1 {
        transition accept;
    }
}

parser simple(packet_in packet);
package top(simple e);
top(p()) main;

@jafingerhut Skimming through the code, I see some examples where we are only checking whether the switch case label expression, rather than its components (if it is a list expression) is a IR::DefaultExpression. So we are certainly not considering (default, default) to be equivalent to default in these cases:

...
    if (keySet->is<IR::DefaultExpression>()) return Result::Yes;
...
                if (!c->keyset->is<IR::DefaultExpression>()) changes = true;
...
                    if (c->label->is<IR::DefaultExpression>()) hasDefault = true;
...

The above is not an exhaustive list of places that I noticed this.

@kfcripps Thanks for pointing those out.

My main worry is if the final IR output by the compiler is functionally different from each other, not whether the IR is 100% the same, since obviously many different IR data structures can be functionally equivalent.

I realize it is not ideal if the warnings are different, but I have to admit that bothers me less.

I would guess that if you really wanted all of the select expressions that are equivalent to default to produce the same IR, and thus hopefully the same warnings and all other behavior, it should not be difficult to recognize all of those that are equivalent to default, since they should be equivalent to (default, default, ...) for as many key expressions are in the select expression.

The way I teach P4 programming is to only use _ (DONTCARE) inside tuples and to use default only "standalone". I think this makes things much easier to read and understand.
I see that the grammar is more permissive, but maybe it would make sense to tighten it.

The way I teach P4 programming is to only use _ (DONTCARE) inside tuples and to use default only "standalone". I think this makes things much easier to read and understand. I see that the grammar is more permissive, but maybe it would make sense to tighten it.

Yeah, that's why I said that the fact that the compiler accepts this "seemed odd." Maybe this deserves an issue in https://github.com/p4lang/p4-spec as well? Regardless of whether this is allowed by the language or not, I think changes to the compiler are likely needed to either treat e.g. (default, default) semantically the same as default or error out.

I realize it is not ideal if the warnings are different, but I have to admit that bothers me less.

@jafingerhut The warning itself is not a big deal. It just makes me suspicious that the compiler could be processing e.g. (default, default) incorrectly in other more critical paths as well. So further investigation to either rule out or correct such problems would be useful.

The construct keysetexpression is used in two places: in the select() statement and in the entries= table property. While I can accept equivalencing default (DEFAULT) and _ (DONTCARE) in the select() statement clauses (although I do not think this is a good idea), it is something that should not be done inside the entries= property as this is definitely going to be confusing.

default and _ have been synonyms for each other in a select key set expression since 2016 in the spec.

default and _ have been synonyms for each other in a const entries entry definition since they were added to the spec in 2017.

I would urge us not to create backwards incompatibility without a really, really good reason.