8. Expressions
This section describes all expressions that can be used in P4, grouped by the type of value they produce.
- The grammar production rule for general expressions is as follows:
Begin P4Grammar expression : INTEGER | DOTS // DOTS is … | STRING_LITERAL | TRUE | FALSE | prefixedNonTypeName | expression ‘[’ expression ’]’ | expression ‘[’ expression ’:’ expression ’]’ | ‘{’ expressionList optTrailingComma ‘}’ | “{#}” | ‘{’ kvList optTrailingComma ‘}’ | “{” kvList “,” DOTS optTrailingComma “}” | ‘(’ expression ‘)’ | ‘!’ expression | ‘\~’ expression | ‘-’ expression | ‘+’ expression | typeName ‘.’ member | ERROR ‘.’ member | expression ‘.’ member | expression ’*’ expression | expression ‘/’ expression | expression ‘%’ expression | expression ‘+’ expression | expression ‘-’ expression | expression ‘|+|’ expression | expression ‘|-|’ expression | expression SHL expression // SHL is \<\< | expression ‘>’‘>’ expression // check that >> are contiguous | expression LE expression // LE is \<= | expression GE expression // GE is >= | expression ‘\<’ expression | expression ‘>’ expression | expression NE expression // NE is != | expression EQ expression // EQ is == | expression ‘&’ expression | expression ‘^’ expression | expression ‘|’ expression | expression PP expression // PP is ++ | expression AND expression // AND is && | expression OR expression // OR is || | expression ‘?’ expression ‘:’ expression | expression ‘\<’ realTypeArgumentList ‘>’ ‘(’ argumentList ‘)’ | expression ‘(’ argumentList ‘)’ | namedType ‘(’ argumentList ‘)’ | ‘(’ typeRef ‘)’ expression ;
[INCLUDE=grammar.mdk:expressionList]
[INCLUDE=grammar.mdk:member]
[INCLUDE=grammar.mdk:argumentList]
[INCLUDE=grammar.mdk:nonEmptyArgList]
argument : expression ;
[INCLUDE=grammar.mdk:typeArg]
- [INCLUDE=grammar.mdk:typeArgumentList]
End P4Grammar
See Appendix [#sec-grammar] for the complete P4 grammar.
This grammar does not indicate the precedence of the various operators.
The precedence mostly follows the C precedence rules, with one change
and some additions. The precedence of the bitwise operators & | and
^ is higher than the precedence of relation operators <, <=, >,
>=. This is more natural given the addition of a true boolean type in
the type system, as bitwise operators cannot be applied to boolean
types. Concatenation (++) has the same precedence as infix addition.
Bit-slicing a[m:l] has the same precedence as array indexing (a[i]).
In addition to these expressions, P4 also supports select expressions
(described in Section [#sec-select]), which may be used only in
parsers.
Given a compound expression, the order in which sub-expressions are evaluated is important when the sub-expressions have side-effects. P4 expressions are evaluated as follows:
- Boolean operators
&&and||use short-circuit evaluation—i.e., the second operand is only evaluated if necessary. - The conditional operator
e1 ? e2 : e3evaluatese1, and then either evaluatese2ore3. - All other expressions are evaluated left-to-right as they appear in the source program.
- Method and function calls are evaluated as described in Section [#sec-calling-convention].
Symbolic names declared by an error declaration belong to the error
namespace. The error type only supports equality (==) and inequality
(!=) comparisons. The result of such a comparison is a Boolean value.
For example, the following operation tests for the occurrence of an error: \~ Begin P4Example error errorFromParser;
-
if (errorFromParser != error.NoError) { /* code omitted */ }
End P4Example -
Symbolic names declared by an enum belong to the namespace introduced by the enum declaration rather than the top-level namespace.
Begin P4Example enum X { v1, v2, v3 } X.v1 // reference to v1 v1 // error - v1 is not in the top-level namespaceEnd P4Example
Similar to errors, enum expressions without a specified underlying
type only support equality (==) and inequality (!=) comparisons.
Expressions whose type is an enum without a specified underlying type
cannot be cast to or from any other type.
-
An
enummay also specify an underlying type, such as the following:
Begin P4Example enum bit\<8> E { e1 = 0, e2 = 1, e3 = 2 }End P4Example
More than one symbolic value in an enum may map to the same
fixed-width integer value.
\~ Begin P4Example enum bit\<8> NonUnique { b1 = 0, b2 = 1, // Note, both b2 and b3 map to the same value. b3 = 1, b4 = 2 } \~ End P4Example
An enum with an underlying type also supports explicit casts to and
from the underlying type. For instance, the following code:
\~ Begin P4Example bit\<8> x; E a = E.e2; E b;
x = (bit\<8>) a; // sets x to 1 b = (E) x; // sets b to E.e2 \~ End P4Example
… casts a (which was initialized with E.e2) to a bit<8>, using the
specified fixed-width unsigned integer representation for E.e2,
i.e. 1. The variable b is then set to the symbolic value E.e2,
which corresponds to the fixed-width unsigned integer value 1.
Because it is always safe to cast from an enum to its underlying
fixed-width integer type, implicit casting from an enum to its
fixed-width (signed or unsigned) integer type is also supported (see
Section [#sec-implicit-casts]):
\~ Begin P4Example bit\<8> x = E.e2; // sets x to 1 (E.e2 is automatically casted to bit\<8>)
E a = E.e2 bit\<8> y = a \<\< 3; // sets y to 8 (a is automatically casted to bit\<8> and then shifted) \~ End P4Example
- Implicit casting from an underlying fixed-width type to an enum is
not supported.
Begin P4Example enum bit\<8> E1 { e1 = 0, e2 = 1, e3 = 2 }
enum bit\<8> E2 { e1 = 10, e2 = 11, e3 = 12 } E1 a = E1.e1; E2 b = E2.e2;
a = b; // Error: b is automatically casted to bit\<8>, // but bit\<8> cannot be automatically casted to E1
a = (E1) b; // OK
a = E1.e1 + 1; // Error: E.e1 is automatically casted to bit\<8>, // and the right-hand expression has // the type bit\<8>, which cannot be casted to E automatically.
a = (E1)(E1.e1 + 1); // Final explicit casting makes the assignment legal
a = E1.e1 + E1.e2; // Error: both arguments to the addition are automatically // casted to bit\<8>. Thus the addition itself is legal, but // the assignment is not
-
a = (E1)(E2.e1 + E2.e2); // Final explicit casting makes the assignment legal
End P4Example -
A reasonable compiler might generate a warning in cases that involve multiple automatic casts.
Begin P4Example E1 a = E1.e1; E2 b = E2.e2; bit\<8> c;
if (a > b) { // Potential warning: two automatic and different casts to bit\<8>. // code omitted }
- c = a + b; // Legal, but a warning would be reasonable
End P4Example
Note that while it is always safe to cast from an enum to its
fixed-width unsigned integer type, and vice versa, there may be cases
where casting a fixed-width unsigned integer value to its related enum
type produces an unnamed value.
\~ Begin P4Example bit\<8> x = 5; E e = (E) x; // sets e to an unnamed value \~ End P4Example
sets e to an unnamed value, since there is no symbol corresponding to
the fixed-width unsigned integer value 5.
For example, in the following code, the else clause of the if/else
if/else block can be reached even though the matches on x are
complete with respect to the symbols defined in MyPartialEnum_t:
\~ Begin P4Example enum bit\<2> MyPartialEnum_t { VALUE_A = 2w0, VALUE_B = 2w1, VALUE_C = 2w2 }
bit\<2> y = \< some value >; MyPartialEnum_t x = (MyPartialEnum_t)y;
if (x == MyPartialEnum_t.VALUE_A) { // some code here } else if (x == MyPartialEnum_t.VALUE_B) { // some code here } else if (x == MyPartialEnum_t.VALUE_C) { // some code here } else { // A P4 compiler MUST ASSUME that this branch can be executed // some code here } \~ End P4Example
Additionally, if an enumeration is used as a field of a header, we would
expect the transition select to match default when the parsed
integer value does not match one of the symbolic values of EtherType
in the following example:
\~ Begin P4Example enum bit\<16> EtherType { VLAN = 0x8100, IPV4 = 0x0800, IPV6 = 0x86dd }
header ethernet { // Some fields omitted EtherType etherType; }
parser my_parser(/* parameters omitted */) { state parse_ethernet { packet.extract(hdr.ethernet); transition select(hdr.ethernet.etherType) { EtherType.VLAN : parse_vlan; EtherType.IPV4 : parse_ipv4; EtherType.IPV6: parse_ipv6; default: reject; } } \~ End P4Example
Any variable with an enum type that contains an unnamed value (whether
as the result of a cast to an enum with an underlying type, parse into
the field of an enum with an underlying type, or simply the
declaration of any enum without a specified initial value) will not be
equal to any of the values defined for that type. Such an unnamed value
should still lead to predictable behavior in cases where any legal value
would match, e.g. it should match in any of these situations:
- If used in a
selectexpression, it should matchdefaultor_in a key set expression. - If used as a key with
match_kindternaryin a table, it should match a table entry where the field has all bit positions “don’t care”. - If used as a key with
match_kindlpmin a table, it should match a table entry where the field has a prefix length of 0.
Note that if an enum value lacking an underlying type appears in the
control-plane API, the compiler must select a suitable serialization
data type and representation. For enum values with an underlying type
and representations, the compiler should use the specified underlying
type as the serialization data type and representation.
Additionally, the size of a serializable enum can be determined at compile-time. However, the size of an enum without an underlying type cannot be determined at compile-time (Section [#sec-minsizeinbits]).
Values of type match_kind are similar to enum values. They support
only assignment and comparisons for equality and inequality.
\~ Begin P4Example match_kind { fuzzy } const bool same = exact == fuzzy; // always ‘false’ \~ End P4Example
The following operations are provided on Boolean expressions:
- “And”, denoted by
&& - “Or”, denoted by
|| - Negation, denoted by
! - Equality and inequality tests, denoted by
==and!=respectively.
The precedence of these operators is similar to C and uses short-circuited evaluation where relevant.
Additionally, the size of a boolean can be determined at compile-time (Section [#sec-minsizeinbits]).
P4 does not implicitly cast from bit-strings to Booleans or vice versa. As a consequence, a program that is valid in a language like C such as,
\~ Begin P4Example if (x) /* body omitted */ \~ End P4Example
-
(where x has an integer type) must instead be written in P4 as:
Begin P4Example if (x != 0) /* body omitted */End P4Example
See the discussion on arbitrary-precision types and implicit casts in
Section [#sec-implicit-casts] for details on how the 0 in this
expression is evaluated.
Conditional operator
A conditional expression of the form e1 ? e2 : e3 behaves the same as
in languages like C. As described above, the expression e1 is
evaluated first, and second either e2 or e3 is evaluated depending
on the result.
The first sub-expression e1 must have Boolean type and the second and
third sub-expressions must have the same type, which cannot both be
arbitrary-precision integers unless the condition itself can be
evaluated at compilation time. This restriction is designed to ensure
that the width of the result of the conditional expression can be
inferred statically at compile time.
This section discusses all operations that can be performed on
expressions of type bit<W> for some width W, also known as
bit-strings.
Arithmetic operations “wrap around”, similar to C operations on unsigned values (i.e., representing a large value on W bits will only keep the least-significant W bits of the value). In particular, P4 does not have arithmetic exceptions—the result of an arithmetic operation is defined for all possible inputs.
P4 target architectures may optionally support saturating arithmetic.
All saturating operations are limited to a fixed range between a minimum
and maximum value. Saturating arithmetic has advantages, in particular
when used as counters. The result of a saturating counter max-ing out is
much closer to the real result than a counter that overflows and wraps
around. According to Wikipedia Saturating
Arithmetic
saturating arithmetic is as numerically close to the true answer as
possible; for 8-bit binary signed arithmetic, when the correct answer is
130, it is considerably less surprising to get an answer of 127 from
saturating arithmetic than to get an answer of −126 from modular
arithmetic. Likewise, for 8-bit binary unsigned arithmetic, when the
correct answer is 258, it is less surprising to get an answer of 255
from saturating arithmetic than to get an answer of 2 from modular
arithmetic. At this time, P4 defines saturating operations only for
addition and subtraction. For an unsigned integer with bit-width of W,
the minimum value is 0 and the maximum value is 2^W-1. The
precedence of saturating addition and subtraction operations is the same
as for modular arithmetic addition and subtraction.
All binary operations except shifts and concatenation require both operands to have the same exact type and width; supplying operands with different widths produces an error at compile time. No implicit casts are inserted by the compiler to equalize the widths. There are no other binary operations that accept signed and unsigned values simultaneously besides shifts and concatenation. The following operations are provided on bit-string expressions:
- Test for equality between bit-strings of the same width, designated
by
==. The result is a Boolean value. - Test for inequality between bit-strings of the same width,
designated by
!=. The result is a Boolean value. - Unsigned comparisons
<,>,<=,>=. Both operands must have the same width and the result is a Boolean value.
Each of the following operations produces a bit-string result when applied to bit-strings of the same width:
- Negation, denoted by unary
-. The result is computed by subtracting the value from 2W. The result is unsigned and has the same width as the input. The semantics is the same as the C negation of unsigned numbers. - Unary plus, denoted by
+. This operation behaves like a no-op. - Addition, denoted by
+. This operation is associative. The result is computed by truncating the result of the addition to the width of the output (similar to C). - Subtraction, denoted by
-. The result is unsigned, and has the same type as the operands. It is computed by adding the negation of the second operand (similar to C). - Multiplication, denoted by
*. The result has the same width as the operands and is computed by truncating the result to the output’s width. P4 architectures may impose additional restrictions — e.g., they may only allow multiplication by a non-negative integer power of two. - Bitwise “and” between two bit-strings of the same width, denoted by
&. - Bitwise “or” between two bit-strings of the same width, denoted by
|. - Bitwise “complement” of a single bit-string, denoted by
~. - Bitwise “xor” of two bit-strings of the same width, denoted by
^. - Saturating addition, denoted by
|+|. - Saturating subtraction, denoted by
|-|.
Bit-strings also support the following operations:
- Logical shift left and right by a non-negative integer value (which
need not be a compile-time known value), denoted by
<<and>>respectively. In a shift, the left operand is unsigned, and right operand must be either an expression of typebit<S>or a non-negative integer value that is known at compile time. The result has the same type as the left operand. Shifting by an amount greater than or equal to the width of the input produces a result where all bits are zero. - Extraction of a set of contiguous bits, also known as a slice,
denoted by
[H:L], whereHandLmust be expressions that evaluate to non-negative, local compile-time known values, andH >= L. The types ofHandL(which do not need to be identical) must be numeric (Section [#sec-numeric-values]). The result is a bit-string of widthH - L + 1, including the bits numbered fromL(which becomes the least significant bit of the result) toH(the most significant bit of the result) from the source operand. The conditions0 <= L <= H < Ware checked statically (whereWis the length of the source bit-string). Note that both endpoints of the extraction are inclusive. The bounds are required to be local compile-time known values so that the width of the result can be computed at compile time. Slices are also l-values, which means that P4 supports assigning to a slice:e[H:L] = x. The effect of this statement is to set bitsHthroughL(inclusive of both) ofeto the bit-pattern represented byx, and leaves all other bits ofeunchanged. A slice of an unsigned integer is an unsigned integer. - Concatenation of bit-strings and/or fixed-width signed integers,
denoted by
++. The two operands must be eitherbit<W>orint<W>, and they can be of different signedness and width. The result has the same signedness as the left operand and the width equal to the sum of the two operands’ width. In concatenation, the left operand is placed as the most significant bits.
Additionally, the size of a bit-string can be determined at compile-time (Section [#sec-minsizeinbits]).
This section discusses all operations that can be performed on
expressions of type int<W> for some W. Recall that the int<W>
denotes signed W-bit integers, represented using two’s complement.
In general, P4 arithmetic operations do not detect “underflow” or
“overflow”: operations simply “wrap around”, similar to C operations
on unsigned values. Hence, attempting to represent large values using
W bits will only keep the least-significant W bits of the value.
P4 supports saturating arithmetic (addition and subtraction) for signed
integers. Targets may optionally reject programs using saturating
arithmetic. For a signed integer with bit-width of W, the minimum
value is -2^(W-1) and the maximum value is 2^(W-1)-1.
P4 also does not support arithmetic exceptions. The runtime result of an arithmetic operation is defined for all combinations of input arguments.
All binary operations except shifts and concatenation require both operands to have the same exact type (signedness) and width and supplying operands with different widths or signedness produces a compile-time error. No implicit casts are inserted by the compiler to equalize the types. Except for shifts and concatenation, P4 does not have any binary operations that operate simultaneously on signed and unsigned values.
Note that bitwise operations on signed integers are well-defined, since the representation is mandated to be two’s complement.
The int<W> datatype supports the following operations; all binary
operations require both operands to have the exact same type. The result
always has the same width as the left operand.
- Negation, denoted by unary
-. - Unary plus, denoted by
+. This operation behaves like a no-op. - Addition, denoted by
+. - Subtraction, denoted by
-. - Comparison for equality and inequality, denoted
==and!=respectively. These operations produce a Boolean result. - Numeric comparisons, denoted by
<,<=,>,and>=. These operations produce a Boolean result. - Multiplication, denoted by
*. Result has the same width as the operands. P4 architectures may impose additional restrictions—e.g., they may only allow multiplication by a power of two. - Bitwise “and” between two bit-strings of the same width, denoted by
&. - Bitwise “or” between two bit-strings of the same width, denoted by
|. - Bitwise “complement” of a single bit-string, denoted by
~. - Bitwise “xor” of two bit-strings of the same width, denoted by
^. - Saturating addition, denoted by
|+|. - Saturating subtraction, denoted by
|-|.
The int<W> datatype also support the following operations:
- Arithmetic shift left and right denoted by
<<and>>. The left operand is signed and the right operand must be either an unsigned number of typebit<S>or a compile-time known value that is a non-negative integer. The result has the same type as the left operand. Shifting left produces the exact same bit pattern as a shift left of an unsigned value. Shift left can thus overflow, when it leads to a change of the sign bit. Shifting by an amount greater than the width of the input produces a “correct” result:- all result bits are zero when shifting left
- all result bits are zero when shifting a non-negative value right
- all result bits are one when shifting a negative value right
- Extraction of a set of contiguous bits, also known as a slice,
denoted by
[H:L], whereHandLmust be expressions that evaluate to non-negative, local compile-time known values, andH >= Lmust be true. The types ofHandL(which do not need to be identical) must be numeric (Section [#sec-numeric-values]). The result is an unsigned bit-string of widthH - L + 1, including the bits numbered fromL(which becomes the least significant bit of the result) toH(the most significant bit of the result) from the source operand. The conditions0 <= L <= H < Ware checked statically (whereWis the length of the source bit-string). Note that both endpoints of the extraction are inclusive. The bounds are required to be values that are known at compile time so that the width of the result can be computed at compile time. Slices are also l-values, which means that P4 supports assigning to a slice:e[H:L] = x. The effect of this statement is to set bitsHthroughLofeto the bit-pattern represented byx, and leaves all other bits ofeunchanged. A slice of a signed integer is treated as an unsigned integer. - Concatenation of bit-strings and/or fixed-width signed integers,
denoted by
++. The two operands must be eitherbit<W>orint<W>, and they can be of different signedness and width. The result has the same signedness as the left operand and the width equal to the sum of the two operands’ width. In concatenation, the left operand is placed as the most significant bits.
Additionally, the size of a fixed-width signed integer can be determined at compile-time (Section [#sec-minsizeinbits]).
The type int denotes arbitrary-precision integers. In P4, all
expressions of type int must be compile-time known values. The type
int supports the following operations:
-
Negation, denoted by unary
- -
Unary plus, denoted by
+. This operation behaves like a no-op. -
Addition, denoted by
+. -
Subtraction, denoted by
-. -
Comparison for equality and inequality, denoted by
==and!=respectively. These operations produce a Boolean result. -
Numeric comparisons
<,<=,>, and>=. These operations produce a Boolean result. -
Multiplication, denoted by
*. -
Truncating integer division between positive values, denoted by
/. -
Modulo between positive values, denoted by
%. -
Arithmetic shift left and right denoted by
<<and>>. These operations produce anintresult. The right operand must be either an unsigned value of typebit<S>or a compile-time known value that is a non-negative integer. The expressiona << bis equal to (a \times 2^b) whilea >> bis equal to (\lfloor{a / 2^b}\rfloor). -
Bit slices, denoted by
[H:L], whereHandLmust be expressions that evaluate to non-negative, local compile-time known values, andH >= Lmust be true. The types ofHandL(which do not need to be identical) must be one of the following:int- an arbitrary-precision integer (section [#sec-arbitrary-precision-integers])bit<W>- aW-bit unsigned integer whereW >= 0(section [#sec-unsigned-integers])int<W>- aW-bit signed integer whereW >= 1(section [#sec-signed-integers])- a serializable
enumwith an underlying type that isbit<W>orint<W>(section [#sec-enum-types]).
The result is an unsigned bit-string of width
H - L + 1, including the bits numbered fromL(which becomes the least significant bit of the result) toH(the most significant bit of the result) from the source operand. The conditions0 <= L <= Hare checked statically. If necessary, the source integer value that is sliced is automatically extended to have a width withHbits. Note that both endpoints of the extraction are inclusive. The bounds are required to be values that are known at compile time so that the width of the result can be computed at compile time. A slice of a negative or positive value is always a positive value.
Each operand that participates in any of these operation must have type
int (except shifts). Binary operations cannot be used to combine
values of type int with values of a fixed-width type (except shifts).
However, the compiler automatically inserts casts from int to
fixed-width types in certain situations—see Section [#sec-casts].
All computations on int values are carried out without loss of
information. For example, multiplying two 1024-bit values may produce a
2048-bit value (note that concrete representation of int values is not
specified). int values can be cast to bit<w> and int<w> values.
Casting an int value to a fixed-width type will preserve the
least-significant bits. If truncation causes significant bits to be
lost, the compiler should emit a warning.
Note: bitwise-operations (|,&,^,~) are not defined on
expressions of type int. In addition, it is illegal to apply division
and modulo to negative values.
Note: saturating arithmetic is not supported for arbitrary-precision integers.
Concatenation
Concatenation is applied to two bit-strings (signed or unsigned). It is
denoted by the infix operator ++. The result is a bit-string whose
length is the sum of the lengths of the inputs where the most
significant bits are taken from the left operand; the sign of the result
is taken from the left operand.
A note about shifts
The left operand of shifts can be any one out of unsigned bit-strings,
signed bit-strings, and arbitrary-precision integers, and the right
operand of shifts must be either an expression of type bit<S> or a
compile-time known value that is a non-negative integer. The result has
the same type as the left operand.
Shifts on signed and unsigned bit-strings deserve a special discussion for the following reasons:
- Right shift behaves differently for signed and unsigned bit-strings: right shift for signed bit-strings is an arithmetic shift, and for unsigned bit-strings is a logical shift.
- Shifting with a negative amount does not have a clear semantics: the P4 type system makes it illegal to shift with a negative amount.
- Unlike C, shifting by an amount larger than or equal to the number of bits has a well-defined result.
- Finally, depending on the capabilities of the target, shifting may require doing work which is exponential in the number of bits of the right-hand-side operand.
-
Consider the following examples:
Begin P4Example bit\<8> x; bit\<16> y; bit\<16> z = y \<\< x; bit\<16> w = y \<\< 1024;End P4Example
As mentioned above, P4 gives a precise meaning shifting with an amount larger than the size of the shifted value, unlike C.
P4 targets may impose additional restrictions on shift operations such as forbidding shifts by non-constant expressions, or by expressions whose width exceeds a certain bound. For example, a target may forbid shifting an 8-bit value by a non-constant value whose width is greater than 3 bits.
To support parsing headers with variable-length fields, P4 offers a type
varbit. Each occurrence of the type varbit has a statically-declared
maximum width, as well as a dynamic width, which must not exceed the
static bound. Prior to initialization a variable-size bit-string has an
unknown dynamic width.
Variable-length bit-strings support a limited set of operations:
- Assignment to another variable-sized bit-string. The target of the assignment must have the same static width as the source. When executed, the assignment sets the dynamic width of the target to the dynamic width of the source.
- Comparison for equality or inequality with another
varbitfield. Twovarbitfields can be compared only if they have the same type. Two varbits are equal if they have the same dynamic width and all the bits up to the dynamic width are the same.
The following operations are not supported directly on a value of type
varbit, but instead on any type for which extract and emit
operations are supported (e.g. a value with type header) that may
contain a field of type varbit. They are mentioned here only to ease
finding this information in a section dedicated to type varbit.
- Parser extraction into a header containing a variable-sized
bit-string using the two-argument
extractmethod of apacket_inextern object (see Section [#sec-packet-extract-two]). This operation sets the dynamic width of the field. - The
emitmethod of apacket_outextern object can be performed on a header and a few other types (see Section [#sec-deparse]) that contain a field with typevarbit. Such anemitmethod call inserts a variable-sized bit-string with a known dynamic width into the packet being constructed.
Additionally, the maximum size of a variable-length bit-string can be determined at compile-time (Section [#sec-minsizeinbits]).
P4 provides a limited set of casts between types. A cast is written (t)
e, where t is a type and e is an expression. Casts are only
permitted on base types and derived types introduced by typedef,
type, and enum. While this design is arguably more onerous for
programmers, it has several benefits:
- It makes user intent unambiguous.
- It makes the costs associated with converting numeric values explicit. Implementing certain casts involves sign extensions, and thus can require significant computational resources on some targets.
- It reduces the number of cases that have to be considered in the P4 specification. Some targets may not support all casts.
Explicit casts
The following casts are legal in P4:
bit<1>↔bool: converts the value0tofalse, the value1totrue, and vice versa.int→bool: only if theintvalue is0(converted tofalse) or1(converted totrue)int<W>→bit<W>: preserves all bits unchanged and reinterprets negative values as positive valuesbit<W>→int<W>: preserves all bits unchanged and reinterprets values whose most-significant bit is1as negative valuesbit<W>→bit<X>: truncates the value ifW > X, and otherwise (i.e., ifW <= X) pads the value with zero bits.int<W>→int<X>: truncates the value ifW > X, and otherwise (i.e., ifW < X) extends it with the sign bit.bit<W>→int: preserves the value unchanged but converts it to an unlimited-precision integer; the result is always non-negativeint<W>→int: preserves the value unchanged but converts it to an unlimited-precision integer; the result may be negativeint→bit<W>: converts the integer value into a sufficiently large two’s complement bit string to avoid information loss, and then truncates the result toWbits. The compiler should emit a warning on overflow or on conversion of negative value.int→int<W>: converts the integer value into a sufficiently-large two’s complement bit string to avoid information loss, and then truncates the result toWbits. The compiler should emit a warning on overflow.- casts between two types that are introduced by
typedefand are equivalent to one of the above combinations. - casts between a typedef and the original type.
- casts between a type introduced by
typeand the original type. - casts between an
enumwith an explicit type and its underlying type - casts of a key-value list to a struct type or a header type (see Section [#sec-structure-expressions])
- casts of a tuple expression to a header stack type
- casts of an invalid expression
{#}to a header or a header union type - casts where the destination type is the same as the source type if the destination type appears in this list (this excludes e.g., parsers or externs).
Implicit casts { sec-implicit-casts }
To keep the language simple and avoid introducing hidden costs, P4 only
implicitly casts from int to fixed-width types and from enums with an
underlying type to the underlying type. In particular, applying a binary
operation (except shifts and concatenation) to an expression of type
int and an expression with a fixed-width type will implicitly cast the
int expression to the type of the other expression. For enums with an
underlying type, it can be implicitly cast to its underlying type
whenever appropriate, including but not limited to in shifts,
concatenation, bit slicing indexes, header stack indexes as well as
other unary and binary operations.
- For example, given the following declarations,
Begin P4Example enum bit\<8> E { a = 5 }
bit\<8> x; bit\<16> y; int\<8> z; \~ End P4Example
the compiler will add implicit casts as follows:
x + 1becomesx + (bit<8>)1z < 0becomesz < (int<8>)0x | 0xFFFbecomesx | (bit<8>)0xFFF; overflow warningx + E.abecomesx + (bit<8>)E.ax &&& 8becomesx &&& (bit<8>)8x << 256remains unchanged;256not implicitly cast to8w0in a shift; overflow warning16w11 << E.abecomes16w11 << (bit<8>)E.ax[E.a:0]becomesx[(bit<8>)E.a:0]E.a ++ 8w0becomes(bit<8>)E.a ++ 8w0
The compiler also adds implicit casts when types of different
expressions need to match; for example, as described in Section
[#sec-select], since select labels are compared against the selected
expression, the compiler will insert implicit casts for the select
labels when they have int types. Similarly, when assigning a
structure-valued expression to a structure or header, the compiler will
add implicit casts for int fields.
Illegal arithmetic expressions
Many arithmetic expressions that would be allowed in other languages are illegal in P4. To illustrate, consider the following declarations:
\~ Begin P4Example bit\<8> x; bit\<16> y; int\<8> z; \~ End P4Example
The table below shows several expressions which are illegal because they do not obey the P4 typing rules. For each expression we provide several ways that the expression could be manually rewritten into a legal expression. Note that for some expression there are several legal alternatives, which may produce different results! The compiler cannot guess the user intent, so P4 requires the user to disambiguate.
|—–|—–|—–| | Expression | Why it is illegal | Alternatives |
+—————-:+:———————+:—————–+ | x + y | Different widths |
(bit<16>)x + y| | | | x + (bit<8>)y | | x + z | Different
signedness | (int<8>)x + z | | | | x + (bit<8>)z | | (int<8>)y |
Cannot change both sign and width | (int<8>)(bit<8>)y | | | |
(int<8>)(int<16>)y | | y + z | Different widths and signs |
(int<8>)(bit<8>)y + z | | | | y + (bit<16>)(bit<8>)z | | | |
(bit<8>)y + (bit<8>)z | | | | (int<16>)y + (int<16>)z | | x << z |
RHS of shift cannot be signed | x << (bit<8>)z | | x < z | Different
signs | x < (bit<8>)z | | | | (int<8>)x < z | | 1 << x | Either
LHS should have a fixed width (bit shift), | 32w1 << x | | | Or RHS
must be compile-time known (int shift) | None | | ~1 | Bitwise
operation on int | ~32w1 | | 5 & -3 | Bitwise operation on int |
32w5 & -3 | |—–|—–|—–|
Tuples can be assigned to other tuples with the same type, passed as arguments and returned from functions, and can be initialized with tuple expressions.
\~ Begin P4Example tuple\<bit\<32>, bool> x = { 10, false }; \~ End P4Example
The fields of a tuple can be accessed using array index syntax x[0],
x[1]. The indexes must be local compile-time known values, to enable
the type-checker to identify the field types statically.
Tuples can be compared for equality using == and !=; two tuples are
equal if and only if all their fields are respectively equal.
Currently tuple fields are not left-values, even if the tuple itself is. (I.e. a tuple can only be assigned monolithically, and the field values cannot be changed individually.) This restriction may be lifted in a future version of the language.
A tuple expression is written using curly braces, with each element separated by a comma:
\~ Begin P4Grammar expression … | ‘{’ expressionList ‘}’
- [INCLUDE=grammar.mdk:expressionList]
End P4Grammar
The type of a tuple expression is a tuple type (Section
[#sec-tuple-types]). Tuple expressions can be assigned to expressions
of type tuple, struct or header, and can also be passed as
arguments to methods. Tuples may be nested. However, tuple expressions
are not l-values.
For example, the following program fragment uses a tuple expression to pass several header fields simultaneously to a learning provider:
\~ Begin P4Example extern LearningProvider
LearningProvider\<tuple\<bit\<48>, bit\<32>>>() lp;
- lp.learn( { hdr.ethernet.srcAddr, hdr.ipv4.src } );
End P4Example
A tuple may be used to initialize a structure if the tuple has the same number of elements as fields in the structure. The effect of such an initializer is to assign the nth element of the tuple to the nth field in the structure:
\~ Begin P4Example struct S { bit\<32> a; bit\<32> b; } const S x = { 10, 20 }; //a = 10, b = 20 \~ End P4Example
A tuple expression can have an explicit structure or header type specified, and then it is converted automatically to a structure-valued expression (see [#sec-structure-expressions]):
\~ Begin P4Example struct S { bit\<32> a; bit\<32> b; }
extern void f
- f((S){ 10, 20 }); // automatically converted to f((S){a = 10, b =
20});
End P4Example
Tuple expressions can also be used to initialize variables whose type is
a tuple type.
\~ Begin P4Example tuple\<bit\<32>, bool> x = { 10, false }; \~ End P4Example
The empty tuple expression has type tuple<> - a tuple with no
components.
One can write expressions that evaluate to a structure or header. The syntax of these expressions is given by:
\~ Begin P4Grammar expression … | ‘{’ kvList ‘}’ | ‘(’ typeRef ‘)’ expression ;
[INCLUDE=grammar.mdk:kvList]
- [INCLUDE=grammar.mdk:kvPair]
End P4Grammar
For a structure-valued expression typeRef is the name of a struct or
header type. The typeRef can be omitted if it can be inferred from
context, e.g., when initializing a variable with a struct type.
Structure-valued expressions that evaluate to a value of some header
type are always valid.
The following example shows a structure-valued expression used in an equality comparison expression:
\~ Begin P4Example struct S { bit\<32> a; bit\<32> b; }
S s;
// Compare s with a structure-valued expression bool b = s == (S) { a = 1, b = 2 }; \~ End P4Example
Structure-valued expressions can be used in the right-hand side of assignments, in comparisons, in field selection expressions, and as arguments to functions, method or actions. Structure-valued expressions are not left values.
Structure-valued expressions that do not have ... as their last
element must provide a value for every member of the struct or header
type to which it evaluates, by mentioning each field name exactly once.
Structure-valued expressions that have ... as their last element are
allowed to give values to only a subset of the fields of the struct or
header type to which it evaluates. Any field names not given a value
explicitly will be given their default value (see Section
[#sec-initializing-with-default-values]).
The order of the fields of the struct or header type does not need
to match the order of the values of the structure-valued expression.
It is a compile-time error if a field name appears more than once in the same structure-valued expression.
The value of a list is written using curly braces, with each element
separated by a comma. The left curly brace is preceded by a (list<T>)
where T is the list element type. Such a value can be passed as an
argument, e.g. to extern constructor functions.
\~ Begin P4Example struct pair_t { bit\<16> a; bit\<32> b; }
extern E { E(list
control c() { E((list
Additionally, the size of a list can be determined at compile-time (Section [#sec-minsizeinbits]).
Some P4 expressions denote sets of values (set<T>, for some type T;
see Section [#sec-set-types]). These expressions can appear only in a
few contexts—parsers and table entries. For example, the select
expression (Section [#sec-select]) has the following structure:
\~ Begin P4Example select (expression) { set1: state1; set2: state2; // More labels omitted } \~ End P4Example
Here the expressions set1, set2, etc. evaluate to sets of values and
the select expression tests whether expression belongs to the sets
used as labels.
\~ Begin P4Grammar [INCLUDE=grammar.mdk:keysetExpression]
[INCLUDE=grammar.mdk:tupleKeysetExpression]
[INCLUDE=grammar.mdk:simpleExpressionList]
[INCLUDE=grammar.mdk:reducedSimpleKeysetExpression]
- [INCLUDE=grammar.mdk:simpleKeysetExpression]
End P4Grammar
The mask (&&&) and range (..) operators have the same precedence;
the just above the ?: operator.
Singleton sets
In a set context, expressions denote singleton sets. For example, in the following program fragment,
\~ Begin P4Example select (hdr.ipv4.version) { 4: continue; } \~ End P4Example
The label 4 denotes the singleton set containing the int value 4.
The universal set
In a set context, the expressions default or _ denote the universal
set, which contains all possible values of a given type:
\~ Begin P4Example select (hdr.ipv4.version) { 4: continue; _: reject; } \~ End P4Example
Masks
The infix operator &&& takes two arguments of the same numeric type
(Section [#sec-numeric-values]), and creates a value of the same
type. The right value is used as a “mask”, where each bit set to 0 in
the mask indicates a “don’t care” bit. More formally, the set denoted by
a &&& b is defined as follows:
\~ Begin P4Example a &&& b = { c where a & b = c & b } \~ End P4Example
-
For example:
Begin P4Example 8w0x0A &&& 8w0x0FEnd P4Example
denotes a set that contains 16 different bit<8> values, whose
bit-pattern is XXXX1010, where the value of an X can be any bit.
Note that there may be multiple ways to express a keyset using a mask
operator—e.g., 8w0xFA &&& 8w0x0F denotes the same keyset as in the
example above.
Similar to other binary operations, the mask operator allows the compiler to automatically insert casts to unify the argument types in certain situations (section [#sec-implicit-casts]).
P4 architectures may impose additional restrictions on the expressions on the left and right-hand side of a mask operator: for example, they may require that either or both sub-expressions be compile-time known values.
Ranges
The infix operator .. takes two arguments of the same numeric type T
(Section [#sec-numeric-values]), and creates a value of the type
set<T>. The set contains all values numerically between the first and
the second, inclusively. For example:
\~ Begin P4Example 4s5 .. 4s8 \~ End P4Example
denotes a set of 4 consecutive int<4> values 4s5, 4s6, 4s7, and
4s8.
Similar to other binary operations, the range operator allows the compiler to automatically insert casts to unify the argument types in certain situations (section [#sec-implicit-casts]).
A range where the second value is smaller than the first one represents an empty set.
Products
-
Multiple sets can be combined using Cartesian product:
Begin P4Example select(hdr.ipv4.ihl, hdr.ipv4.protocol) { (4w0x5, 8w0x1): parse_icmp; (4w0x5, 8w0x6): parse_tcp; (4w0x5, 8w0x11): parse_udp; (, ): accept; }End P4Example
The type of a product of sets is a set of tuples.
The only operation defined on expressions whose type is a struct is
field access, written using dot (“.”) notation—e.g., s.field. If s
is an l-value, then s.field is also an l-value. P4 also allows copying
structs using assignment when the source and target of the assignment
have the same type. Finally, structs can be initialized with a tuple
expression, as discussed in Section [#sec-tuple-exprs], or with a
structure-valued expression, as described in
[#sec-structure-expressions]. Both of these cases must initialize all
fields of the structure. The size of a struct can be determined at
compile-time (Section [#sec-minsizeinbits]).
Two structs can be compared for equality (==) or inequality (!=) only if they have the same type and all of their fields can be recursively compared for equality. Two structures are equal if and only if all their corresponding fields are equal.
The following example shows a structure initialized in several different ways:
\~ Begin P4Example struct S { bit\<32> a; bit\<32> b; } const S x = { 10, 20 }; // tuple expression const S x = { a = 10, b = 20 }; // structure-valued expression const S x = (S) { a = 10, b = 20 }; // structure-valued expression \~ End P4Example
See Section [#sec-uninitialized-values-and-writing-invalid-headers]
for a description of the behavior if struct fields are read without
being initialized.
Headers provide the same operations as structs. Assignment between
headers also copies the “validity” header bit.
In addition, headers support the following methods:
- The method
isValid()returns the value of the “validity” bit of the header. - The method
setValid()sets the header’s validity bit to “true”. It can only be applied to an l-value. - The method
setInvalid()sets the header’s validity bit to “false”. It can only be applied to an l-value.
Similar to a struct, a header object can be initialized with a tuple
expression (see Section [#sec-tuple-exprs]) — the tuple fields are
assigned to the header fields in the order they appear — or with a
structure-valued expression (see Section [#sec-ops-on-structs]). When
initialized the header automatically becomes valid:
\~ Begin P4Example header H { bit\<32> x; bit\<32> y; } H h; h = { 10, 12 }; // This also makes the header h valid h = { y = 12, x = 10 }; // Same effect as above \~ End P4Example
Two headers can be compared for equality (==) or inequality (!=)
only if they have the same type. Two headers are equal if and only if
they are both invalid, or they are both valid and all their
corresponding fields are equal. Furthermore, the size of a header can be
determined at compile-time (Section [#sec-minsizeinbits]).
The expression {#} represents an invalid header of some type, but it
can be any header or header union type. A P4 compiler may require an
explicit cast on this expression in cases where it cannot determine the
particular header or header union type from the context.
\~ Begin P4Grammar expression … | “{#}” \~ End P4Grammar
-
For example:
Begin P4Example header H { bit\<32> x; bit\<32> y; } H h; h = {#}; // This make the header h become invalid if (h == {#}) { // This is equivalent to the condition !h.isValid() // … }End P4Example
Note that the # character cannot be misinterpreted as a preprocessor
directive, since it cannot be the first character on a line when it
occurs in the single lexical token {#}, which may not have whitespace
or any other characters between those shown.
See Section [#sec-uninitialized-values-and-writing-invalid-headers] for a description of the behavior if header fields are read without being initialized, or header fields are written to a currently invalid header.
A header stack is a fixed-size array of headers with the same type. The
valid elements of a header stack need not be contiguous. P4 provides a
set of computations for manipulating header stacks. A header stack hs
of type h[n] can be understood in terms of the following pseudocode:
\~ Begin P4Pseudo // type declaration struct hs_t { bit\<32> nextIndex; bit\<32> size; h[n] data; // Ordinary array }
// instance declaration and initialization hs_t hs; hs.nextIndex = 0; hs.size = n; \~ End P4Pseudo
Intuitively, a header stack can be thought of as a struct containing an
ordinary array of headers hs and a counter nextIndex that can be
used to simplify the construction of parsers for header stacks, as
discussed below. The nextIndex counter is initialized to 0.
Given a header stack value hs of size n, the following expressions
are legal:
-
hs[index]: produces a reference to the header at the specified position within the stack; ifhsis an l-value, the result is also an l-value. The header may be invalid. Some implementations may impose the constraint that the index expression must be a compile-time known value. A P4 compiler must give an error if an index that is a compile-time known value is out of range.Accessing a header stack
hswith an index less than0or greater than or equal tohs.sizeresults in an undefined value. See Section [#sec-uninitialized-values-and-writing-invalid-headers] for more details.The
indexis an expression that must be of numeric types (Section [#sec-numeric-values]). -
hs.size: produces a 32-bit unsigned integer that returns the size of the header stack (a local compile-time known value). -
assignment from a header stack
hsinto another stack requires the stacks to have the same types and sizes. All components ofhsare copied, including its elements and their validity bits, as well asnextIndex.
To help programmers write parsers for header stacks, P4 also offers computations that automatically advance through the stack as elements are parsed:
-
hs.next: produces a reference to the element with indexhs.nextIndexin the stack. May only be used in aparser. If the stack’snextIndexcounter is greater than or equal tosize, then evaluating this expression results in a transition torejectand sets the error toerror.StackOutOfBounds. Ifhsis an l-value, thenhs.nextis also an l-value. -
hs.last: produces a reference to the element with indexhs.nextIndex - 1in the stack, if such an element exists. May only be used in aparser. If thenextIndexcounter is less than1, or greater thansize, then evaluating this expression results in a transition torejectand sets the error toerror.StackOutOfBounds. Unlikehs.next, the resulting reference is never an l-value. -
hs.lastIndex: produces a 32-bit unsigned integer that encodes the indexhs.nextIndex - 1. May only be used in aparser. If thenextIndexcounter is0, then evaluating this expression produces an undefined value.
Finally, P4 offers the following computations that can be used to manipulate the elements at the front and back of the stack:
-
hs.push_front(int count): shiftshs“right” bycount. The firstcountelements become invalid. The lastcountelements in the stack are discarded. Thehs.nextIndexcounter is incremented bycount. Thecountargument must be a compile-time known value that is a positive integer. The return type isvoid. -
hs.pop_front(int count): shiftshs“left” bycount(i.e., element with indexcountis copied in stack at index0). The lastcountelements become invalid. Thehs.nextIndexcounter is decremented bycount. Thecountargument must be a compile-time known value that is a positive integer. The return type isvoid.
- The following pseudocode defines the behavior of
push_frontandpop_front:
Begin P4Pseudo void push_front(int count) { for (int i = this.size-1; i >= 0; i -= 1) { if (i >= count) { this[i] = this[i-count]; } else { this[i].setInvalid(); } } this.nextIndex = this.nextIndex + count; if (this.nextIndex > this.size) this.nextIndex = this.size; // Note: this.last, this.next, and this.lastIndex adjust with this.nextIndex }
void pop_front(int count) { for (int i = 0; i \< this.size; i++) { if (i+count \< this.size) { this[i] = this[i+count]; } else { this[i].setInvalid(); } } if (this.nextIndex >= count) { this.nextIndex = this.nextIndex - count; } else { this.nextIndex = 0; } // Note: this.last, this.next, and this.lastIndex adjust with this.nextIndex } \~ End P4Pseudo
Similar to structs and headers, the size of a header stack is a compile-time known value (Section [#sec-minsizeinbits]).
Two header stacks can be compared for equality (==) or inequality
(!=) only if they have the same element type and the same length. Two
stacks are equal if and only if all their corresponding elements are
equal. Note that the nextIndex value is not used in the equality
comparison.
Header stack expressions
One can write expressions that evaluate to a header stack. The syntax of these expressions is given by:
\~ Begin P4Grammar expression … | ‘{’ expressionList ‘}’ | ‘(’ typeRef ‘)’ expression ; \~ End P4Grammar
The typeRef is a header stack type. The typeRef can be omitted if it
can be inferred from context, e.g., when initializing a variable with a
header stack type. Each expression in the list must evaluate to a header
of the same type as the other stack elements.
- Here is an example:
Begin P4Example header H{ bit\<32> b; T t; } H\<bit\<32>>[3] s = (H\<bit\<32>>[3]){ {0, 1}, {2, 3}, (H\<bit\<32>>){#} }; // without an explicit cast H\<bit\<32>>[3] s1 = { {0, 1}, {2, 3}, (H\<bit\<32>>){#} }; // using the default initializer H\<bit\<32>>[3] s2 = { {0, 1}, {2, 3}, … }; \~End P4Example
The values of s, s1, and s2 in the above example are identical.
A variable declared with a union type is initially invalid. For example:
\~Begin P4Example header H1 { bit\<8> f; } header H2 { bit\<16> g; } header_union U { H1 h1; H2 h2; }
U u; // u invalid \~End P4Example
This also implies that each of the headers h1 through hn contained
in a header union are also initially invalid. Unlike headers, a union
cannot be initialized. However, the validity of a header union can be
updated by assigning a valid header to one of its elements:
\~Begin P4Example U u; H1 my_h1 = { 8w0 }; // my_h1 is valid u.h1 = my_h1; // u and u.h1 are both valid \~End P4Example
We can also assign a tuple to an element of a header union,
\~Begin P4Example U u; u.h2 = { 16w1 }; // u and u.h2 are both valid \~End P4Example
or set their validity bits directly.
\~Begin P4Example U u; u.h1.setValid(); // u and u.h1 are both valid H1 my_h1 = u.h1; // my_h1 is now valid, but contains an undefined value \~End P4Example
Note that reading an uninitialized header produces an undefined value, even if the header is itself valid.
If u is an expression whose type is a header union U with fields
ranged over by hi, then the expression u.hi evaluates to a header,
and thus it can be used wherever a header expression is allowed. If u
is a left-value, then u.hi is also a left-value.
The following operations:
-
u.hi.setValid():sets the valid bit for headerhitotrueand sets the valid bit for all other headers tofalse, which implies that it is unspecified what value reading any member header ofuwill return. -
u.hi.setInvalid(): if the valid bit for any member header ofuistruethen sets it tofalse, which implies that it is unspecified what value reading any member header ofuwill return.
The assignment to a union field:
\~Begin P4Example u.hi = e; \~End P4Example
has the following meaning:
- if
eis valid, then it is equivalent to:
\~Begin P4Example u.hi.setValid(); u.hi = e; \~End P4Example
- if
eis invalid, then it is equivalent to:
\~Begin P4Example u.hi.setInvalid(); \~End P4Example
Assignments between variables of the same type of header union are
permitted. The assignment u1 = u2 copies the full state of header
union u2 to u1. If u2 is valid, then there is some header u2.hi
that is valid. The assignment behaves the same as u1.hi = u2.hi. If
u2 is not valid, then u1 becomes invalid (i.e. if any header of u1
was valid, it becomes invalid).
u.isValid() returns true if any member of the header union u is
valid, otherwise it returns false. setValid() and setInvalid()
methods are not defined for header unions.
Supplying an expression with a union type to emit simply emits the
single header that is valid, if any.
The following example shows how we can use header unions to represent IPv4 and IPv6 headers uniformly:
\~Begin P4Example header_union IP { IPv4 ipv4; IPv6 ipv6; }
struct Parsed_packet { Ethernet ethernet; IP ip; }
parser top(packet_in b, out Parsed_packet p) { state start { b.extract(p.ethernet); transition select(p.ethernet.etherType) { 16w0x0800 : parse_ipv4; 16w0x86DD : parse_ipv6; } } state parse_ipv4 { b.extract(p.ip.ipv4); transition accept; } state parse_ipv6 { b.extract(p.ip.ipv6); transition accept; } } \~End P4Example
As another example, we can also use unions to parse (selected) TCP options: \~Begin P4Example header Tcp_option_end_h { bit\<8> kind; } header Tcp_option_nop_h { bit\<8> kind; } header Tcp_option_ss_h { bit\<8> kind; bit\<32> maxSegmentSize; } header Tcp_option_s_h { bit\<8> kind; bit\<24> scale; } header Tcp_option_sack_h { bit\<8> kind; bit\<8> length; varbit\<256> sack; } header_union Tcp_option_h { Tcp_option_end_h end; Tcp_option_nop_h nop; Tcp_option_ss_h ss; Tcp_option_s_h s; Tcp_option_sack_h sack; }
typedef Tcp_option_h[10] Tcp_option_stack;
struct Tcp_option_sack_top { bit\<8> kind; bit\<8> length; }
parser Tcp_option_parser(packet_in b, out Tcp_option_stack vec) {
state start { transition select(b.lookahead\<bit\<8>>()) { 8w0x0 :
parse_tcp_option_end; 8w0x1 : parse_tcp_option_nop; 8w0x2 :
parse_tcp_option_ss; 8w0x3 : parse_tcp_option_s; 8w0x5 :
parse_tcp_option_sack; } } state parse_tcp_option_end {
b.extract(vec.next.end); transition accept; } state
parse_tcp_option_nop { b.extract(vec.next.nop); transition start; }
state parse_tcp_option_ss { b.extract(vec.next.ss); transition start;
} state parse_tcp_option_s { b.extract(vec.next.s); transition start;
} state parse_tcp_option_sack { bit\<8> n =
b.lookahead
Similar to headers, the size of a header union is a local compile-time known value (Section [#sec-minsizeinbits]).
The expression {#} represents an invalid header union of some type,
but it can be any header or header union type. A P4 compiler may require
an explicit cast on this expression in cases where it cannot determine
the particular header or header union type from the context.
\~ Begin P4Example header_union HU { … } HU h = (HU){#}; // invalid header union; same as an uninitialized header union. \~ End P4Example
Two header unions can be compared for equality (==) or inequality
(!=) if they have the same type. The unions are equal if and only if
all their corresponding fields are equal (i.e., either all fields are
invalid in both unions, or in both unions the same field is valid, and
the values of the valid fields are equal as headers).
Method invocations and function calls can be invoked using the following syntax:
\~ Begin P4Grammar expression : … | expression ‘\<’ realTypeArgumentList ‘>’ ‘(’ argumentList ‘)’ | expression ‘(’ argumentList ‘)’
[INCLUDE=grammar.mdk:argumentList]
[INCLUDE=grammar.mdk:nonEmptyArgList]
[INCLUDE=grammar.mdk:argument]
[INCLUDE=grammar.mdk:realTypeArgumentList]
- [INCLUDE=grammar.mdk:realTypeArg]
End P4Grammar
A function call or method invocation can optionally specify for each argument the corresponding parameter name. It is illegal to use names only for some arguments: either all or no arguments must specify the parameter name. Function arguments are evaluated in the order they appear, left to right, before the function invocation takes place.
\~ Begin P4Example extern void f(in bit\<32> x, out bit\<16> y); bit\<32> xa = 0; bit\<16> ya; f(xa, ya); // match arguments by position f(x = xa, y = ya); // match arguments by name f(y = ya, x = xa); // match arguments by name in any order //f(x = xa); – error: enough arguments //f(x = xa, x = ya); – error: argument specified twice //f(x = xa, ya); – error: some arguments specified by name //f(z = xa, w = yz); – error: no parameter named z or w //f(x = xa, y = 0); – error: y must be a left-value \~ End P4Example
The calling convention is copy-in/copy-out (Section
[#sec-calling-convention]). For generic functions the type arguments
can be explicitly specified in the function call. The compiler only
inserts implicit casts for direction in arguments to methods or
functions as described in Section [#sec-casts]. The types for all
other arguments must match the parameter types exactly.
The result returned by a function call is discarded when the function call is used as a statement.
The “don’t care” identifier (_) can only be used for an out
function/method argument, when the value of returned in that argument is
ignored by subsequent computations. When used in generic functions or
methods, the compiler may reject the program if it is unable to infer a
type for the don’t care argument.
Several P4 constructs denote resources that are allocated at compilation time:
externobjectsparserscontrolblockspackages
Allocation of such objects can be performed in two ways:
- Using constructor invocations, which are expressions that return an object of the corresponding type.
- Using instantiations, described in Section [#sec-instantiations].
The syntax for a constructor invocation is similar to a function call; constructors can also be called using named arguments. Constructors are evaluated entirely at compilation time (see Section [#sec-p4-abstract-mach]). In consequence, all constructor arguments must also be expressions that can be evaluated at compilation time. When performing type inference and overload resolution, constructor invocations are treated similar to methods or functions.
The following example shows a constructor invocation for setting the target-dependent implementation property of a table:
\~ Begin P4Example extern ActionProfile { ActionProfile(bit\<32> size); // constructor } table tbl { actions = { /* body omitted */ } implementation = ActionProfile(1024); // constructor invocation } \~ End P4Example
The only operations that can be performed on extern objects are the
following:
- Instantiating an extern object using a constructor invocation, as described in Section [#sec-constructor].
- Invoking a method of an extern object instance using a method call expression, as described in Section [#sec-invocations].
Controls, parsers, packages, and extern constructors can have parameters
of type extern only if they are directionless parameters. Extern
object instances can thus be used as arguments in the construction of
such objects.
No other operations are available on extern objects, e.g., equality comparison is not defined.
Values with a type introduced by the type keyword provide only a few
operations:
- assignment to left-values of the same type
- comparisons for equality and inequality if the original type supported such comparisons
- casts to and from the original type
\~ Begin P4Example type bit\<32> U32; U32 x = (U32)0; // cast needed U32 y = (U32) ((bit\<32>)x + 1); // casts needed for arithmetic bit\<32> z = 1; bool b0 = x == (U32)z; // cast needed bool b1 = (bit\<32>)x == z; // cast needed bool b2 = x == y; // no cast needed \~ End P4Example
Because functions, methods, control, and parsers can be generic, they offer the possibility of declaring values with types that are type variables:
\~ Begin P4Example control C
The type of such objects is not known until the control is instantiated with specific type arguments.
Currently the only operations that are available for such values are
assignment (explicit through =, or implicit, through argument
passing). This behavior is similar to languages such as Java, and
different from languages such as C++.
A future version of P4 may introduce a notion of type constraints which would enable more operations on such values. Because of this limitation, such values are currently of limited utility.
As mentioned in Section [#sec-expr-hs], any reference to an element
of a header stack hs[index] where index is a compile-time known
value must give an error at compile time if the value of the index is
out of range. That section also defines the run time behavior of the
expressions hs.next and hs.last, and the behaviors specified there
take precedence over anything in this section for those expressions.
All mentions of header stack elements in this section only apply for
expressions hs[index] where index is not a compile-time known value.
A P4 implementation may elect not to support expressions of the form
hs[index] where index is not a compile-time known value. However, it
does support such expressions, the implementation should conform to the
behaviors specified in this section.
The result of reading a value in any of the situations below is that some unspecified value will be used for that field.
- reading a field from a header that is currently invalid.
- reading a field from a header that is currently valid, but the field has not been initialized since the header was last made valid.
- reading any other value that has not been initialized, e.g. a field
from a
struct, any uninitialized variable inside of anactionorcontrol, or anoutparameter of acontroloractionyou have called, which was not assigned a value during the execution of thatcontroloraction(this list of examples is not intended to be exhaustive). - reading a field of a header that is an element of a header stack, where the index is out of range for the header stack.
Calling the isValid() method on an element of a header stack, where
the index is out of range, returns an undefined boolean value, i.e., it
is either true or false, but the specification does not require one
or the other, nor that a consistent value is returned across multiple
such calls. Assigning an out-of-range header stack element to another
header variable h leads to a state where h is undefined in all of
its field values, and its validity is also undefined.
Where a header is mentioned, it may be a member of a header_union, an
element in a header stack, or a normal header. This unspecified value
could differ from one such read to another.
For an uninitialized field or variable with a type of enum or error,
the unspecified value that is read might not be equal to any of the
values defined for that type. Such an unspecified value should still
lead to predictable behavior in cases where any legal value would match,
e.g. it should match in any of these situations:
- If used in a
selectexpression, it should matchdefaultor_in a key set expression. - If used as a key with
match_kindternaryin a table, it should match a table entry where the field has all bit positions “don’t care”. - If used as a key with
match_kindlpmin a table, it should match a table entry where the field has a prefix length of 0.
Consider a situation where a header_union u1 has member headers
u1.h1 and u1.h2, and at a given point in the program’s execution
u1.h1 is valid and u1.h2 is invalid. If a write is attempted to a
field of the invalid member header u1.h2, then any or all of the
fields of the valid member header u1.h1 may change as a result. Such a
write must not change the validity of any member headers of u1, nor
any other state that is currently defined in the system, whether it is
defined state in header fields or anywhere else.
If any of these kinds of writes are performed:
- a write to a field in a currently invalid header, either a regular
header or an element of a header stack with an index that is in
range, and that header is not part of a
header_union - a write to a field in an element of a header stack, where the index is out of range
- a method call of
setValid()orsetInvalid()on an element of a header stack, where the index is out of range
then that write must not change any state that is currently defined in the system, neither in header fields nor anywhere else. In particular, if an invalid header is involved in the write, it must remain invalid.
Any writes to fields in a currently invalid header, or to header stack elements where the index is out of range, are allowed to modify state whose values are not defined, e.g. the values of fields in headers that are currently invalid.
For a top level parser or control in an architecture, it is up to
that architecture to specify whether parameters with direction in or
inout are initialized when the control is called, and under what
conditions they are initialized, and if so, what their values will be.
Since P4 allows empty tuples and structs, one can construct types whose values carry no “useful” information, e.g.:
\~Begin P4Example struct Empty { tuple\<> t; } \~End P4Example
We call the following “empty” types:
- bitstrings with 0 width
- varbits with 0 width
- empty tuples (
tuple<>) - stacks with 0 size
- structs with no fields
- a tuple having all fields of an empty type
- a struct having all fields of an empty type
Values with empty types carry no useful information. In particular, they do not have to be explicitly initialized to have a legal value.
(Header types with no fields always have a validity bit.)
A left-value can be initialized automatically with a default value of
the suitable type using the syntax ... (see Section
[#sec-default-values]). A value of type struct, header, or
tuple can also be initialized using a mix of explicit values and
default values by using the notation ... in a tuple expression
initializer; in this case all fields not explicitly initialized are
initialized with default values. When initializing a struct, header,
and tuple with a value containing partially default values using the
... notation the three dots must appear last in the initializer.
\~ Begin P4Example struct S { bit\<32> b32; bool b; }
enum int\<8> N0 { one = 1, zero = 0, two = 2 }
enum N1 { A, B, C, F }
struct T { S s; N0 n0; N1 n1; }
header H { bit\<16> f1; bit\<8> f2; }
N0 n0 = …; // initialize n0 with the default value 0 N1 n1 = …; // initialize n1 with the default value N1.A S s0 = …; // initialize s0 with the default value { 0, false } S s1 = { 1, … }; // initialize s1 with the value { 1, false } S s2 = { b = true, … }; // initialize s2 with the value { 0, true } T t0 = …; // initialize t0 with the value { { 0, false }, 0, N1.A } T t1 = { s = …, … }; // initialize t1 with the value { { 0, false }, 0, N1.A } T t2 = { s = … }; // error: no initializer specified for fields n0 and n1 tuple\<N0, N1> p = { … }; // initialize p with default value { 0, N1.A } T t3 = { …, n0 = 2}; // error: … must be last H h1 = …; // initialize h1 with a header that is invalid H h2 = { f2=5, … }; // initialize h2 with a header that is valid, field f1 0, // field f2 5 H h3 = { … }; // initialize h3 with a header that is valid, field f1 0, field f2 0 \~ End P4Example