コンテンツにスキップ

7. P4 data types

P416 is a statically-typed language. Programs that do not pass the type checker are considered invalid and rejected by the compiler. P4 provides a number of base types as well as type operators that construct derived types. Some values can be converted to a different type using casts. However, to make user intents clear, implicit casts are only allowed in a few circumstances and the range of casts available is intentionally restricted.

P4 supports the following built-in base types:

  • The void type, which has no values and can be used only in a few restricted circumstances.
  • The error type, which is used to convey errors in a target-independent, compiler-managed way.
  • The string type, which can be used with compile-time known values of type string.
  • The match_kind type, which is used for describing the implementation of table lookups,
  • bool, which represents Boolean values
  • int, which represents arbitrary-sized integer values
  • Bit-strings of fixed width, denoted by bit<>
  • Fixed-width signed integers represented using two’s complement int<>
  • Bit-strings of dynamically-computed width with a fixed maximum width varbit<>

\~ Begin P4Grammar [INCLUDE=grammar.mdk:baseType] \~ End P4Grammar

The void type

The void type is written void. It contains no values. It is not included in the production rule baseType as it can only appear in few restricted places in P4 programs.

The error type

The error type contains opaque distinct values that can be used to signal errors. It is written as error. New elements of the error type are defined with the syntax:

\~ Begin P4Grammar [INCLUDE=grammar.mdk:errorDeclaration] \~ End P4Grammar

All elements of the error type are inserted into the error namespace, irrespective of the place where an error is defined. error is similar to an enumeration (enum) type in other languages. A program can contain multiple error declarations, which the compiler will merge together. It is an error to declare the same identifier multiple times. Expressions of type error are described in Section [#sec-error-exprs].

For example, the following declaration creates two elements of the error type (these errors are declared in the P4 core library):

\~ Begin P4Example error { ParseError, PacketTooShort } \~ End P4Example

The underlying representation of errors is target-dependent.

The match kind type

The match_kind type is very similar to the error type and is used to declare a set of distinct names that may be used in a table’s key property (described in Section [#sec-table-props]). All identifiers are inserted into the top-level namespace. It is an error to declare the same match_kind identifier multiple times.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:matchKindDeclaration] \~ End P4Grammar

  • The P4 core library contains the following match_kind declaration:
    Begin P4Example match_kind { exact, ternary, lpm }

    End P4Example

Architectures may support additional match_kinds. The declaration of new match_kinds can only occur within model description files; P4 programmers cannot declare new match kinds.

Operations on values of type match_kind are described in Section [#sec-match-kind-exprs].

The Boolean type

The Boolean type bool contains just two values, false and true. Boolean values are not integers or bit-strings.

Strings

The type string represents strings. There are no operations on string values; one cannot declare variables with a string type. Parameters with type string can be only directionless (see Section [#sec-calling-convention]). P4 does not support string manipulation in the dataplane; the string type is only allowed for describing compile-time known values (i.e., string literals, as discussed in Section [#sec-string-literals]). Even so, the string type is useful, for example, in giving the type signature for extern functions such as the following:

\~ Begin P4Example extern void log(string message); \~ End P4Example

As another example, the following annotation indicates that the specified name should be used for a given table in the generated control-plane API:

\~ Begin P4Example @name(“acl”) table t1 { /* body omitted */ } \~ End P4Example

Integers (signed and unsigned)

P4 supports arbitrary-size integer values. The typing rules for the integer types are chosen according to the following principles:

  • Inspired by C: Typing of integers is modeled after the well-defined parts of C, expanded to cope with arbitrary fixed-width integers. In particular, the type of the result of an expression only depends on the expression operands, and not on how the result of the expression is consumed.
  • No undefined behaviors: P4 attempts to avoid many of C’s behaviors, which include the size of an integer (int), the results produced on overflow, and the results produced for some input combinations (e.g., shifts with negative amounts, overflows on signed numbers, etc.). P4 computations on integer types have no undefined behaviors.
  • Least surprise: The P4 typing rules are chosen to behave as closely as possible to traditional well-behaved C programs.
  • Forbid rather than surprise: Rather than provide surprising or undefined results (e.g., in C comparisons between signed and unsigned integers), we have chosen to forbid expressions with ambiguous interpretations. For example, P4 does not allow binary operations that combine signed and unsigned integers.

The priority of arithmetic operations is identical to C—e.g., multiplication binds tighter than addition.

Portability

No P4 target can support all possible types and operations. For example, the type bit<23132312> is legal in P4, but it is highly unlikely to be supported on any target in practice. Hence, each target can impose restrictions on the types it can support. Such restrictions may include:

  • The maximum width supported
  • Alignment and padding constraints (e.g., arithmetic may only be supported on widths which are an integral number of bytes).
  • Constraints on some operands (e.g., some architectures may only support multiplications by small values, or shifts with small values).

The documentation supplied with a target should clearly specify restrictions, and target-specific compilers should provide clear error messages when such restrictions are encountered. An architecture may reject a well-typed P4 program and still be conformant to the P4 spec. However, if an architecture accepts a P4 program as valid, the runtime program behavior should match this specification.

Unsigned integers (bit-strings)

An unsigned integer (which we also call a “bit-string”) has an arbitrary width, expressed in bits. A bit-string of width W is declared as: bit<W>. W must be an expression that evaluates to a local compile-time known value (see Section [#sec-compile-time-known]) that is a non-negative integer. When using an expression for the size, the expression must be parenthesized. Bitstrings with width 0 are allowed; they have no actual bits, and can only have the value 0. See [#sec-uninitialized-values-and-writing-invalid-headers] for additional details. Note that bit<W> type refers to both cases of bit<W> and bit<(expression)> where the width is a compile-time known value.

\~ Begin P4Example const bit\<32> x = 10; // 32-bit constant with value 10. const bit\<(x + 2)> y = 15; // 12-bit constant with value 15. // expression for width must use () \~ End P4Example

Bits within a bit-string are numbered from 0 to W-1. Bit 0 is the least significant, and bit W-1 is the most significant.

For example, the type bit<128> denotes the type of bit-string values with 128 bits numbered from 0 to 127, where bit 127 is the most significant.

The type bit is a shorthand for bit<1>.

P4 architectures may impose additional constraints on bit types: for example, they may limit the maximum size, or they may only support some arithmetic operations on certain sizes (e.g., 16-, 32-, and 64- bit values).

All operations that can be performed on unsigned integers are described in Section [#sec-bit-ops].

Signed Integers

Signed integers are represented using two’s complement. An integer with W bits is declared as: int<W>. W must be an expression that evaluates to a local compile-time known (see Section [#sec-compile-time-known]) value that is a non-negative integer. Note that int<W> type refers to both cases of int<W> and int<(expression)> where the width is a local compile-time known value.

Bits within an integer are numbered from 0 to W-1. Bit 0 is the least significant, and bit W-1 is the sign bit.

For example, the type int<64> describes the type of integers represented using exactly 64 bits with bits numbered from 0 to 63, where bit 63 is the most significant (sign) bit.

P4 architectures may impose additional constraints on signed types: for example, they may limit the maximum size, or they may only support some arithmetic operations on certain sizes (e.g., 16-, 32-, and 64- bit values).

All operations that can be performed on signed integers are described in Section [#sec-int-ops].

A signed integer with width 1 can only have two legal values: 0 and -1.

Dynamically-sized bit-strings

Some network protocols use fields whose size is only known at runtime (e.g., IPv4 options). To support restricted manipulations of such values, P4 provides a special bit-string type whose size is set at runtime, called a varbit.

The type varbit<W> denotes a bit-string with a width of at most W bits, where W is a local compile-time known value (see Section [#sec-compile-time-known]) that is a non-negative integer. For example, the type varbit<120> denotes the type of bit-string values that may have between 0 and 120 bits. Most operations that are applicable to fixed-size bit-strings (unsigned numbers) cannot be performed on dynamically sized bit-strings. Note that varbit<W> type refers to both cases of varbit<W> and varbit<(expression)> where the width is a compile-time known value.

P4 architectures may impose additional constraints on varbit types: for example, they may limit the maximum size, or they may require varbit values to always contain an integer number of bytes at runtime.

All operations that can be performed on varbits are described in Section [#sec-varbit-string].

Arbitrary-precision integers

The arbitrary-precision data type describes integers with an unlimited precision. This type is written as int.

This type is reserved for integer literals and expressions that involve only literals. No P4 runtime value can have an int type; at compile time the compiler will convert all int values that have a runtime component to fixed-width types, according to the rules described below.

All operations that can be performed on arbitrary-precision integers are described in Section [#sec-varint-ops]. The following example shows three constant definitions whose values are arbitrary-precision integers.

\~ Begin P4Example const int a = 5; const int b = 2 * a; const int c = b - a + 3; \~ End P4Example

Parameters with type int are not supported for actions. Parameters with type int for other callable entities of a program, e.g. controls, parsers, or functions, must be directionless, indicating that all calls must provide a compile-time known value as an argument for such a parameter. See Section [#sec-calling-convention] for more details on directionless parameters.

Integer literal types

The types of integer literals are as follows:

  • An integer with no type prefix has type int.
  • A non-negative integer prefixed with an integer width W and the character w has type bit<W>.
  • An integer prefixed with an integer width W and the character s has type int<W>.

The table below shows several examples of integer literals and their types. For additional examples of literals see Section [#sec-literals].

Literal

10 8w10 8s10 2s3 1w10 1s1 ———

Interpretation

Type is int, value is 10 Type is bit<8>, value is 10 Type is int<8>, value is 10 Type is int<2>, value is -1 (last 2 bits), overflow warning Type is bit<1>, value is 0 (last bit), overflow warning Type is int<1>, value is -1, overflow warning ——————–

P4 provides a number of type constructors that can be used to derive additional types including:

  • enum
  • header
  • header stacks
  • struct
  • header_union
  • tuple
  • type specialization
  • extern
  • parser
  • control
  • package

The types header, header_union, enum, struct, extern, parser, control, and package can only be used in type declarations, where they introduce a new name for the type. The type can subsequently be referred to using this identifier.

Other types cannot be declared, but are synthesized by the compiler internally to represent the type of certain language constructs. These types are described in Section [#sec-synth-types]: set types and function types. For example, the programmer cannot declare a variable with type “set”, but she can write an expression whose value evaluates to a set type. These types are used during type-checking.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:typeDeclaration]

[INCLUDE=grammar.mdk:derivedTypeDeclaration]

[INCLUDE=grammar.mdk:typeRef]

[INCLUDE=grammar.mdk:namedType]

[INCLUDE=grammar.mdk:prefixedType]

  • [INCLUDE=grammar.mdk:typeName]
    End P4Grammar

Enumeration types

  • An enumeration type is defined using the following syntax:
    Begin P4Grammar [INCLUDE=grammar.mdk:enumDeclaration]

[INCLUDE=grammar.mdk:identifierList]

[INCLUDE=grammar.mdk:specifiedIdentifierList]

  • [INCLUDE=grammar.mdk:specifiedIdentifier]
    End P4Grammar

  • For example, the declaration
    Begin P4Example enum Suits { Clubs, Diamonds, Hearths, Spades }

    End P4Example

introduces a new enumeration type, which contains four elements—e.g., Suits.Clubs. An enum declaration introduces a new identifier in the current scope for naming the created type along with its distinct elements. The underlying representation of the Suits enum is not specified, so their “size” in bits is not specified (it is target-specific).

It is also possible to specify an enum with an underlying representation. These are sometimes called serializable enums, because headers are allowed to have fields with such enum types. This requires the programmer provide both the fixed-width unsigned (or signed) integer type and an associated integer value for each symbolic entry in the enumeration. The symbol typeRef in the grammar above must be one of the following types:

  • an unsigned integer, i.e. bit<W> for some local compile-time known value W.
  • a signed integer, i.e. int<W> for some local compile-time known value W.
  • a type name declared via typedef, where the base type of that type is either one of the types listed above, or another typedef name that meets these conditions. For example, the declaration

\~ Begin P4Example enum bit\<16> EtherType { VLAN = 0x8100, QINQ = 0x9100, MPLS = 0x8847, IPV4 = 0x0800, IPV6 = 0x86dd } \~ End P4Example

introduces a new enumeration type, which contains five elements—e.g., EtherType.IPV4. This enum declaration specifies the fixed-width unsigned integer representation for each entry in the enum and provides an underlying type: bit<16>. This kind of enum declaration can be thought of as declaring a new bit<16> type, where variables or fields of this type are expected to be unsigned 16-bit integer values, and the mapping of symbolic to numeric values defined by the enum are also defined as a part of this declaration. In this way, an enum with an underlying type can be thought of as being a type derived from the underlying type carrying equality, assignment, and casts to/from the underlying type.

Compiler implementations are expected to raise an error if the fixed-width integer representation for an enumeration entry falls outside the representation range of the underlying type.

  • For example, the declaration
    Begin P4Example enum bit\<8> FailingExample { first = 1, second = 2, third = 3, unrepresentable = 300 }

    End P4Example

would raise an error because 300, the value associated with FailingExample.unrepresentable cannot be represented as a bit<8> value.

The initializer expression must be a compile-time known value.

Annotations, represented by the non-terminal optAnnotations, are described in Section [#sec-annotations].

Operations on enum values are described in Section [#sec-enum-exprs].

Header types

The declaration of a header type is given by the following syntax:

\~ Begin P4Grammar [INCLUDE=grammar.mdk:headerTypeDeclaration]

[INCLUDE=grammar.mdk:structFieldList]

  • [INCLUDE=grammar.mdk:structField]
    End P4Grammar

where each typeRef is restricted to a bit-string type (fixed or variable), a fixed-width signed integer type, a boolean type, or a struct that itself contains other struct fields, nested arbitrarily, as long as all of the “leaf” types are bit<W>, int<W>, a serializable enum, or a bool. If a bool is used inside a P4 header, all implementations encode the bool as a one bit long field, with the value 1 representing true and 0 representing false. Field names have to be distinct.

A header declaration introduces a new identifier in the current scope; the type can be referred to using this identifier. A header is similar to a struct in C, containing all the specified fields. However, in addition, a header also contains a hidden Boolean “validity” field. When the “validity” bit is true we say that the “header is valid”. When a local variable with a header type is declared, its “validity” bit is automatically set to false. The “validity” bit can be manipulated by using the header methods isValid(), setValid(), and setInvalid(), as described in Section [#sec-ops-on-hdrs].

Note, nesting of headers is not supported. One reason is that it leads to complications in defining the behavior of arbitrary sequences of setValid, setInvalid, and emit operations. Consider an example where header h1 contains header h2 as a member, both currently valid. A program executes h2.setInvalid() followed by packet.emit(h1). Should all fields of h1 be emitted, but skipping h2? Similarly, should h1.setInvalid() invalidate all headers contained within h1, regardless of how deeply they are nested?

  • Header types may be empty:
    Begin P4Example header Empty_h { }

    End P4Example

Note that an empty header still contains a validity bit.

When a struct is inside of a header, the order of the fields for the purposes of extract and emit calls is the order of the fields as defined in the source code. An example of a header including a struct is included below.

\~ Begin P4Example struct ipv6_addr { bit\<32> Addr0; bit\<32> Addr1; bit\<32> Addr2; bit\<32> Addr3; }

header ipv6_t { bit\<4> version; bit\<8> trafficClass; bit\<20> flowLabel; bit\<16> payloadLen; bit\<8> nextHdr; bit\<8> hopLimit; ipv6_addr src; ipv6_addr dst; } \~ End P4Example

Headers that do not contain any varbit field are “fixed size.” Headers containing varbit fields have “variable size.” The size (in bits) of a fixed-size header is simply the sum of the sizes of all component fields (without counting the validity bit). There is no padding or alignment of the header fields. Targets may impose additional constraints on header types—e.g., restricting headers to sizes that are an integer number of bytes.

For example, the following declaration describes a typical Ethernet header:

\~ Begin P4Example header Ethernet_h { bit\<48> dstAddr; bit\<48> srcAddr; bit\<16> etherType; } \~ End P4Example

  • The following variable declaration uses the newly introduced type Ethernet_h:
    Begin P4Example Ethernet_h ethernetHeader;

    End P4Example

P4’s parser language provides an extract method that can be used to “fill in” the fields of a header from a network packet, as described in Section [#sec-packet-data-extraction]. The successful execution of an extract operation also sets the validity bit of the extracted header to true.

  • Here is an example of an IPv4 header with variable-sized options:
    Begin P4Example header IPv4_h { bit\<4> version; bit\<4> ihl; bit\<8> diffserv; bit\<16> totalLen; bit\<16> identification; bit\<3> flags; bit\<13> fragOffset; bit\<8> ttl; bit\<8> protocol; bit\<16> hdrChecksum; bit\<32> srcAddr; bit\<32> dstAddr; varbit\<320> options; }

    End P4Example

As demonstrated by a code example in Section [#sec-packet-extract-two], another way to support headers that contain variable-length fields is to define two headers – one fixed length, one containing a varbit field – and extract each part in separate parsing steps.

Notice that the names isValid, setValid, minSizeInBits, etc. are all valid header field names.

Header stacks

A header stack represents an array of headers or header unions. A header stack type is defined as:

\~ Begin P4Grammar [INCLUDE=grammar.mdk:headerStackType] \~ End P4Grammar

where typeName is the name of a header or header union type. For a header stack hs[n], the term n is the maximum defined size, and must be a local compile-time known value that is a positive integer. Nested header stacks are not supported. At runtime a stack contains n values with type typeName, only some of which may be valid. Expressions on header stacks are discussed in Section [#sec-expr-hs].

  • For example, the following declarations,
    Begin P4Example header Mpls_h { bit\<20> label; bit\<3> tc; bit bos; bit\<8> ttl; } Mpls_h[10] mpls;

    End P4Example

introduce a header stack called mpls containing ten entries, each of type Mpls_h.

Operations on header stacks are described in Section [#sec-expr-hs].

Header unions

A header union represents an alternative containing at most one of several different headers. Header unions can be used to represent “options” in protocols like TCP and IP. They also provide hints to P4 compilers that only one alternative will be present, allowing them to conserve storage resources.

  • A header union is defined as:
    Begin P4Grammar [INCLUDE=grammar.mdk:headerUnionDeclaration]

    End P4Grammar

This declaration introduces a new type with the specified name in the current scope. Each element of the list of fields used to declare a header union must be of header type. An empty list of fields is legal. Field names have to be distinct.

As an example, the type Ip_h below represents the union of an IPv4 and IPv6 headers:

\~ Begin P4Example header_union IP_h { IPv4_h v4; IPv6_h v6; } \~ End P4Example

A header union is not considered a type with fixed length.

Operation on header unions are described in Section [#sec-expr-hu].

Struct types

  • P4 struct types are defined with the following syntax:
    Begin P4Grammar [INCLUDE=grammar.mdk:structTypeDeclaration]

    End P4Grammar

This declaration introduces a new type with the specified name in the current scope. Field names have to be distinct. An empty struct (with no fields) is legal. For example, the structure Parsed_headers below contains the headers recognized by a simple parser:

\~ Begin P4Example header Tcp_h { /* fields omitted / } header Udp_h { / fields omitted */ } struct Parsed_headers { Ethernet_h ethernet; Ip_h ip; Tcp_h tcp; Udp_h udp; } \~ End P4Example

Tuple types

A tuple is similar to a struct, in that it holds multiple values. The type of tuples with n component types T1,…,Tn is written as

\~ Begin P4Example tuple\<T1,/* more fields omitted */,Tn> \~ End P4Example

\~ Begin P4Grammar [INCLUDE=grammar.mdk:tupleType] \~ End P4Grammar

Operations that manipulate tuple types are described in Section [#sec-tuple-exprs].

The type tuple<> is a tuple type with no components.

List types

A list holds zero or more values, where every element must have the same type. The type of a list where all elements have type T is written as

\~ Begin P4Example list \~ End P4Example

\~ Begin P4Grammar [INCLUDE=grammar.mdk:p4listType] \~ End P4Grammar

Operations that manipulate list types are described in Section [#sec-list-exprs].

Type nesting rules

The table below lists all types that may appear as members of headers, header unions, structs, tuples, and lists. Note that int by itself (i.e. not as part of an int<N> type expression) means an arbitrary-precision integer, without a width specified.

|——————–|————————————————||||| | | Container kind ||||| | |———–|————–|—————–|——|————–| | Element type | header | header_union | struct or tuple | list | header stack | +:——————-+:———-+:————-+:—————-+:—–+:————-+ | bit<W> | allowed | error | allowed | allowed | error | | int<W> | allowed | error | allowed | allowed | error | | varbit<W> | allowed | error | allowed | allowed | error | | int | error | error | error | allowed | error | | void | error | error | error | error | error | | string | error | error | error | allowed | error | | error | error | error | allowed | allowed | error | | match_kind | error | error | error | allowed | error | | bool | allowed | error | allowed | allowed | error | | enumeration types | allowed[1] | error | allowed | allowed | error | | header types | error | allowed | allowed | allowed | allowed | | header stacks | error | error | allowed | allowed | error | | header unions | error | error | allowed | allowed | allowed | | struct types | allowed[2] | error | allowed | allowed | error | | tuple types | error | error | allowed | allowed | error | | list types | error | error | error | allowed | error | |——————–|———–|———|———-|———|——-|

Rationale: int does not have precise storage requirements, unlike bit<> or int<> types. match_kind values are not useful to store in a variable, as they are only used to specify how to match fields in table search keys, which are all declared at compile time. void is not useful as part of another data structure. Headers must have precisely defined formats as sequences of bits in order for them to be parsed or deparsed.

Note the two-argument extract method (see Section [#sec-packet-extract-two]) on packets only supports a single varbit field in a header.

The table below lists all types that may appear as base types in a typedef or type declaration.

|——————-|——————–|—————–| | Base type B | typedef B <name> | type B <name> | +:——————+:——————-+:—————-+ | bit<W> | allowed | allowed | | int<W> | allowed | allowed | | varbit<W> | allowed | error | | int | allowed | error | | void | error | error | | string | allowed | error | | error | allowed | error | | match_kind | error | error | | bool | allowed | allowed | | enumeration types | allowed | error | | header types | allowed | error | | header stacks | allowed | error | | header unions | allowed | error | | struct types | allowed | error | | tuple types | allowed | error | | list types | allowed | error | | a typedef name | allowed | allowed[3] | | a type name | allowed | allowed | |——————-|——————–|—————–|

Rationale: So far, no clear motivation for allowing typedef for void and match_kind was presented. Therefore, to be on the safe side this is disallowed.

Synthesized data types

For the purposes of type-checking the P4 compiler can synthesize some type representations which cannot be directly expressed by users. These are described in this section: set types and function types.

Set types

The type set<T> describes sets of values of some type T. Set types can only appear in restricted contexts in P4 programs. For example, the range expression 8w5 .. 8w8 describes a set containing the 8-bit numbers 5, 6, 7, and 8, so its type is set<bit<8>>;. This expression can be used as a label in a select expression (see Section [#sec-select]), matching any value in this range. Set types cannot be named or declared by P4 programmers, they are only synthesized by the compiler internally and used for type-checking. Expressions with set types are described in Section [#sec-set-exprs].

Function types

[]{tex-cmd: “”} Function types are created by the P4 compiler internally to represent the types of functions (explicit functions or extern functions) and methods during type-checking. We also call the type of a function its signature. Libraries can contain functions and extern function declarations.

  • For example, consider the following declarations:
    Begin P4Example extern void random(in bit\<5> logRange, out bit\<32> value);

bit\<32> add(in bit\<32> left, in bit\<32> right) { return left + right; } \~ End P4Example

These declarations describe two objects:

  • random, which has a function type, representing the following information:
    • the result type is void
    • the function has two inputs
    • the first formal parameter has direction in, type bit<5>, and name logRange
    • the second formal parameter has direction out, type bit<32>, and name value
  • add, also has a function type, representing the following information:
    • the result type is bit<32>
    • the function has two inputs
    • both inputs have direction in and type bit<32>

Extern types

[]{tex-cmd: “”} P4 supports extern object declarations and extern function declarations using the following syntax.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:externDeclaration] \~ End P4Grammar

Extern functions

[]{tex-cmd: “”} An extern function declaration describes the name and type signature of the function, but not its implementation.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:functionPrototype] \~ End P4Grammar

For an example of an extern function declaration, see Section [#sec-function-type].

Extern objects

[]{tex-cmd: “”} An extern object declaration declares an object and all methods that can be invoked to perform computations and to alter the state of the object. Extern object declarations can also optionally declare constructor methods; these must have the same name as the enclosing extern type, no type parameters, and no return type. Extern declarations may only appear as allowed by the architecture model and may be specific to a target.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:methodPrototypes]

methodPrototype : optAnnotations functionPrototype ‘;’ | optAnnotations TYPE_IDENTIFIER ‘(’ parameterList ‘)’ ‘;’ //constructor | optAnnotations ABSTRACT functionPrototype “;” ;

[INCLUDE=grammar.mdk:typeOrVoid]

[INCLUDE=grammar.mdk:optTypeParameters]

[INCLUDE=grammar.mdk:typeParameters]

  • [INCLUDE=grammar.mdk:typeParameterList]
    End P4Grammar

For example, the P4 core library introduces two extern objects packet_in and packet_out used for manipulating packets (see Sections [#sec-packet-data-extraction] and [#sec-deparse]). Here is an example showing how the methods of these objects can be invoked on a packet:

\~ Begin P4Example extern packet_out { void emit(in T hdr); } control d(packet_out b, in Hdr h) { apply { b.emit(h.ipv4); // write ipv4 header into output packet } // by calling emit method } \~ End P4Example

Functions and methods are the only P4 constructs that support overloading: there can exist multiple methods with the same name in the same scope. When overloading is used, the compiler must be able to disambiguate at compile-time which method or function is being called, either by the number of arguments or by the names of the arguments, when calls are specifying argument names. Argument type information is not used in disambiguating calls.

  • Notice that overloading of parsers, controls, or packages is not allowed:
    Begin P4Example parser p(packet_in p, out bit\<32> value) { … }

// The following will cause an error about a duplicate declaration //parser p(packet_in p, out Headers headers) { // … //} \~ End P4Example

Abstract methods

Typical extern object methods are built-in, and are implemented by the target architecture. P4 programmers can only call such methods.

However, some types of extern objects may provide methods that can be implemented by the P4 programmers. Such methods are described with the abstract keyword prior to the method definition. Here is an example:

\~ Begin P4Example extern Balancer { Balancer(); // get the number of active flows bit\<32> getFlowCount(); // return port index used for load-balancing // @param address: IPv4 source address of flow abstract bit\<4> on_new_flow(in bit\<32> address); } \~ End P4Example

When such an object is instantiated the user has to supply an implementation of all the abstract methods (see [#sec-instantiating-abstract-methods]).

Type specialization

A generic type may be specialized by specifying arguments for its type variables. In cases where the compiler can infer type arguments type specialization is not necessary. When a type is specialized all its type variables must be bound.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:specializedType] \~ End P4Grammar

For example, the following extern declaration describes a generic block of registers, where the type of the elements stored in each register is an arbitrary T.

\~ Begin P4Example extern Register { Register(bit\<32> size); T read(bit\<32> index); void write(bit\<32> index, T value); } \~ End P4Example

The type T has to be specified when instantiating a set of registers, by specializing the Register type:

\~ Begin P4Example Register\<bit\<32>>(128) registerBank; \~ End P4Example

The instantiation of registerBank is made using the Register type specialized with the bit<32> bound to the T type argument.

struct, header, header_union and header stack types can be generic as well. In order to use such a generic type it must be specialized with appropriate type arguments. For example

\~ Begin P4Example // generic structure type struct S { T field; bool valid; }

struct G { S s; }

// specialize S by replacing ‘T’ with ‘bit\<32>’ const S\<bit\<32>> s = { field = 32w0, valid = false }; // Specialize G by replacing ‘T’ with ‘bit\<32>’ const G\<bit\<32>> g = { s = { field = 0, valid = false } };

// generic header type header H { T field; }

// Specialize H by replacing ‘T’ with ‘bit\<8>’ const H\<bit\<8>> h = { field = 1 }; // Header stack produced from a specialization of a generic header type H\<bit\<8>>[10] stack;

// Generic header union header_union HU { H\<bit\<32>> h32; H\<bit\<8>> h8; H ht; }

// Header union with a type obtained by specializing a generic header union type HU hu; \~ End P4Example

Parser and control blocks types

Parsers and control blocks types are similar to function types: they describe the signature of parsers and control blocks. Such functions have no return values. Declarations of parsers and control block types in architectures may be generic (i.e., have type parameters).

The types parser, control, and package cannot be used as types of arguments for methods, parsers, controls, tables, or actions. They can be used as types for the arguments passed to constructors (see Section [#sec-parameterization]).

Parser type declarations

A parser type declaration describes the signature of a parser. A parser should have at least one argument of type packet_in, representing the received packet that is processed.

\~ Begin P4Grammar [INCLUDE=grammar.mdk:parserTypeDeclaration] \~ End P4Grammar

For example, the following is a type declaration of a parser type named P that is parameterized on a type variable H. That parser receives as input a packet_in value b and produces two values:

  • A value with a user-defined type H
  • A value with a predefined type Counters

\~ Begin P4Example struct Counters { /* Fields omitted */ } parser P(packet_in b, out H packetHeaders, out Counters counters); \~ End P4Example

Control type declarations

  • A control type declaration describes the signature of a control block.
    Begin P4Grammar [INCLUDE=grammar.mdk:controlTypeDeclaration]

    End P4Grammar

Control type declarations are similar to parser type declarations.

Package types

  • A package type describes the signature of a package.
    Begin P4Grammar [INCLUDE=grammar.mdk:packageTypeDeclaration]

    End P4Grammar

All parameters of a package are evaluated at compilation time, and in consequence they must all be directionless (they cannot be in, out, or inout). Otherwise package types are very similar to parser type declarations. Packages can only be instantiated; there are no runtime behaviors associated with them.

Don’t care types

A don’t care (underscore, “_”) can be used in some circumstances as a type. It should be only used in a position where one could write a bound type variable. The underscore can be used to reduce code complexity—when it is not important what the type variable binds to (during type unification the don’t care type can unify with any other type). An example is given Section [#sec-arch-desc-example].

Some P4 types define a “default value,” which can be used to automatically initialize values of that type. The default values are as follows:

  • For int, bit<N> and int<N> types the default value is 0.
  • For bool the default value is false.
  • For error the default value is error.NoError (defined in core.p4)
  • For string the default value is the empty string ""
  • For varbit<N> the default value is a string of zero bits (there is currently no P4 literal to represent such a value).
  • For enum values with an underlying type the default value is 0, even if 0 is actually not one of the named values in the enum.
  • For enum values without an underlying type the default value is the first value that appears in the enum type declaration.
  • For header types the default value is invalid.
  • For header stacks the default value is that all elements are invalid and the nextIndex is 0.
  • For header_union values the default value is that all union elements are invalid.
  • For struct types the default value is a struct where each field has the default value of the suitable field type – if all such default values are defined.
  • For a tuple type the default value is a tuple where each field has the default value of the suitable type – if all such default values are defined.

Note that some types do not have default values, e.g., match_kind, set types, function types, extern types, parser types, control types, package types.

Many P4 operations are restrained to expressions that evaluate to numeric values. Such expressions must have one of the following numeric types:

  • 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]).

A typedef declaration can be used to give an alternative name to a type.

\~ Begin P4Grammar typedefDeclaration : optAnnotations TYPEDEF typeRef name ‘;’ | optAnnotations TYPEDEF derivedTypeDeclaration name ‘;’ ; \~ End P4Grammar

\~ Begin P4Example typedef bit\<32> u32; typedef struct Point { int\<32> x; int\<32> y; } Pt; typedef Empty_h[32] HeaderStack; \~ End P4Example

The two types are treated as synonyms, and all operations that can be executed using the original type can be also executed using the newly created type.

If typedef is used with a generic type the type must be specialized with the suitable number of type arguments:

\~ Begin P4Example struct S { T field; }

// typedef S X; – illegal: S does not have type arguments typedef S\<bit\<32>> X; // – legal \~ End P4Example

Similarly to typedef, the keyword type can be used to introduce a new type.

\~ Begin P4Grammar | optAnnotations TYPE typeRef name \~ End P4Grammar

\~ Begin P4Example type bit\<32> U32; U32 x = (U32)0; \~ End P4Example

While similar to typedef, the type keyword introduces a new type which is not a synonym with the original type: values of the original type and the newly introduced type cannot be mixed in expressions.

Currently the types that can be created by the type keyword are restricted to one of: bit<>, int<>, bool, or types defined using type from such types.

One important use of such types is in describing P4 values that need to be exchanged with the control plane through communication channels (e.g., through the control-plane API or through network packets sent to the control plane). For example, a P4 architecture may define a type for the switch ports:

\~Begin P4Example type bit\<9> PortId_t; \~End P4Example

This declaration will prevent PortId_t values from being used in arithmetic expressions without casts. Moreover, this declaration may enable special manipulation or such values by software that lies outside of the datapath (e.g., a target-specific toolchain could include software that automatically converts values of type PortId_t to a different representation when exchanged with the control-plane software).