コンテンツにスキップ

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 : e3 evaluates e1, and then either evaluates e2 or e3.
  • 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 namespace

    End 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 enum may 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 select expression, it should match default or _ in a key set expression.
  • If used as a key with match_kind ternary in 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_kind lpm in 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 type bit<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], where H and L must be expressions that evaluate to non-negative, local compile-time known values, and H >= L. The types of H and L (which do not need to be identical) must be numeric (Section [#sec-numeric-values]). The result is a bit-string of width H - L + 1, including the bits numbered from L (which becomes the least significant bit of the result) to H (the most significant bit of the result) from the source operand. The conditions 0 <= L <= H < W are checked statically (where W is 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 bits H through L (inclusive of both) of e to the bit-pattern represented by x, and leaves all other bits of e unchanged. 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 either bit<W> or int<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 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. 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], where H and L must be expressions that evaluate to non-negative, local compile-time known values, and H >= L must be true. The types of H and L (which do not need to be identical) must be numeric (Section [#sec-numeric-values]). The result is an unsigned bit-string of width H - L + 1, including the bits numbered from L (which becomes the least significant bit of the result) to H (the most significant bit of the result) from the source operand. The conditions 0 <= L <= H < W are checked statically (where W is 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 bits H through L of e to the bit-pattern represented by x, and leaves all other bits of e unchanged. 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 either bit<W> or int<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 an int result. The right operand must be either an unsigned value of type bit<S> or a compile-time known value that is a non-negative integer. The expression a << b is equal to (a \times 2^b) while a >> b is equal to (\lfloor{a / 2^b}\rfloor).

  • Bit slices, denoted by [H:L], where H and L must be expressions that evaluate to non-negative, local compile-time known values, and H >= L must be true. The types of H and L (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> - a W-bit unsigned integer where W >= 0 (section [#sec-unsigned-integers])
    • int<W> - a W-bit signed integer where W >= 1 (section [#sec-signed-integers])
    • a serializable enum with an underlying type that is bit<W> or int<W> (section [#sec-enum-types]).

    The result is an unsigned bit-string of width H - L + 1, including the bits numbered from L (which becomes the least significant bit of the result) to H (the most significant bit of the result) from the source operand. The conditions 0 <= L <= H are checked statically. If necessary, the source integer value that is sliced is automatically extended to have a width with H bits. 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 varbit field. Two varbit fields 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 extract method of a packet_in extern object (see Section [#sec-packet-extract-two]). This operation sets the dynamic width of the field.
  • The emit method of a packet_out extern object can be performed on a header and a few other types (see Section [#sec-deparse]) that contain a field with type varbit. Such an emit method 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 value 0 to false, the value 1 to true, and vice versa.
  • intbool: only if the int value is 0 (converted to false) or 1 (converted to true)
  • int<W>bit<W>: preserves all bits unchanged and reinterprets negative values as positive values
  • bit<W>int<W>: preserves all bits unchanged and reinterprets values whose most-significant bit is 1 as negative values
  • bit<W>bit<X>: truncates the value if W > X, and otherwise (i.e., if W <= X) pads the value with zero bits.
  • int<W>int<X>: truncates the value if W > X, and otherwise (i.e., if W < 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-negative
  • int<W>int: preserves the value unchanged but converts it to an unlimited-precision integer; the result may be negative
  • intbit<W>: converts the integer value into a sufficiently large two’s complement bit string to avoid information loss, and then truncates the result to W bits. The compiler should emit a warning on overflow or on conversion of negative value.
  • intint<W>: converts the integer value into a sufficiently-large two’s complement bit string to avoid information loss, and then truncates the result to W bits. The compiler should emit a warning on overflow.
  • casts between two types that are introduced by typedef and are equivalent to one of the above combinations.
  • casts between a typedef and the original type.
  • casts between a type introduced by type and the original type.
  • casts between an enum with 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 + 1 becomes x + (bit<8>)1
  • z < 0 becomes z < (int<8>)0
  • x | 0xFFF becomes x | (bit<8>)0xFFF; overflow warning
  • x + E.a becomes x + (bit<8>)E.a
  • x &&& 8 becomes x &&& (bit<8>)8
  • x << 256 remains unchanged; 256 not implicitly cast to 8w0 in a shift; overflow warning
  • 16w11 << E.a becomes 16w11 << (bit<8>)E.a
  • x[E.a:0] becomes x[(bit<8>)E.a:0]
  • E.a ++ 8w0 becomes (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(); void learn(in T data); }

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(in T data);

  • 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 data); void run(); }

control c() { E((list) {{2, 3}, {4, 5}}) e; apply { e.run(); } } \~ End P4Example

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 &&& 8w0x0F

    End 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; if hs is 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 hs with an index less than 0 or greater than or equal to hs.size results in an undefined value. See Section [#sec-uninitialized-values-and-writing-invalid-headers] for more details.

    The index is 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 hs into another stack requires the stacks to have the same types and sizes. All components of hs are copied, including its elements and their validity bits, as well as nextIndex.

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 index hs.nextIndex in the stack. May only be used in a parser. If the stack’s nextIndex counter is greater than or equal to size, then evaluating this expression results in a transition to reject and sets the error to error.StackOutOfBounds. If hs is an l-value, then hs.next is also an l-value.

  • hs.last: produces a reference to the element with index hs.nextIndex - 1 in the stack, if such an element exists. May only be used in a parser. If the nextIndex counter is less than 1, or greater than size, then evaluating this expression results in a transition to reject and sets the error to error.StackOutOfBounds. Unlike hs.next, the resulting reference is never an l-value.

  • hs.lastIndex: produces a 32-bit unsigned integer that encodes the index hs.nextIndex - 1. May only be used in a parser. If the nextIndex counter is 0, 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): shifts hs “right” by count. The first count elements become invalid. The last count elements in the stack are discarded. The hs.nextIndex counter is incremented by count. The count argument must be a compile-time known value that is a positive integer. The return type is void.

  • hs.pop_front(int count): shifts hs “left” by count (i.e., element with index count is copied in stack at index 0). The last count elements become invalid. The hs.nextIndex counter is decremented by count. The count argument must be a compile-time known value that is a positive integer. The return type is void.

  • The following pseudocode defines the behavior of push_front and pop_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 header hi to true and sets the valid bit for all other headers to false, which implies that it is unspecified what value reading any member header of u will return.

  • u.hi.setInvalid(): if the valid bit for any member header of u is true then sets it to false, which implies that it is unspecified what value reading any member header of u will return.

The assignment to a union field:

\~Begin P4Example u.hi = e; \~End P4Example

has the following meaning:

  • if e is valid, then it is equivalent to:

\~Begin P4Example u.hi.setValid(); u.hi = e; \~End P4Example

  • if e is 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().length; // n is the total length of the TCP SACK option in bytes. // The length of the varbit field ‘sack’ of the // Tcp_option_sack_h header is thus n-2 bytes. b.extract(vec.next.sack, (bit\<32>) (8 * n - 16)); transition start; } } \~End P4Example

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:

  • extern objects
  • parsers
  • control blocks
  • packages

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() { apply { T x; // the type of x is T, a type variable } } \~ End P4Example

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 an action or control, or an out parameter of a control or action you have called, which was not assigned a value during the execution of that control or action (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 select expression, it should match default or _ in a key set expression.
  • If used as a key with match_kind ternary in 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_kind lpm in 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() or setInvalid() 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