Declarations
This section covers two spec files:
5.08-typing-declaration.watsup
and
5.12-typing-extern.watsup.
Together they handle the top-level declarations that make up a Nano-P4 program:
- instantiations
- action declarations
- extern declarations
- type declarations
- structs
- headers
- parser/control/package type signatures
- match-kind declarations
- parser declaration
- control declaration
The section also covers the top-level entry point Program_ok, which sequences
all declarations and builds the final typing context.
A recurring theme in this section is the threading pattern seen in earlier
sections: most relations take an incoming context TC_0 and produce an outgoing
context TC_1 after adding whatever names the declaration introduces.
Action Declaration
ActionDecl_ok
relation ActionDecl_ok:
typingContext |- actionDeclaration -| typingContext
hint(input %0 %1)
rule ActionDecl_ok:
TC_0 |- actionDeclaration -| TC_1
-- if ACTION name_action `( parameterList `) blockStatement = actionDeclaration
-- if parameter* = $flatten_parameterList(parameterList)
-- if (direction _ name = parameter)*
-- if $directionless_trailing(direction*)
-- Parameters_ok: LOCAL TC_0 |- parameter* : parameterIR* -| TC_body
-- Block_ok: TC_body |- blockStatement
-- if callableId = $id(name_action)
-- if callableTypeDef = ACTION parameterIR*
-- if TC_1 = $add_callableDef_t(TC_0, callableId, callableTypeDef)
The rule proceeds in three phases.
Structural extraction and direction check. The action is unpacked to obtain
its name, parameter list, and body. The parameter directions are extracted and
passed to $directionless_trailing, which enforces a P4 rule: all directionless
(EMPTY) parameters must appear at the end of the parameter list.
Type-checking the body. Parameters_ok is invoked at LOCAL scope to
elaborate the parameter list and produce TC_body, the context in which the
action body is checked. Note that TC_0 is the outer context passed to
Parameters_ok, so existing names remain visible inside the action. Block_ok
then verifies the body under TC_body.
Registering the action. The action is stored in TC_0’s callable
environment under callableId with type ACTION parameterIR*. The action is
registered in TC_0, not TC_body: the bindings introduced by the parameters
are not visible outside the action.
$directionless_trailing
The helper scans the direction list from the end to enforce the trailing rule:
def $directionless_trailing(direction*)
= $directionless_trailing'(true, $rev_<direction>(direction*))
def $directionless_trailing'(_, eps) = true
def $directionless_trailing'(true, _EMPTY :: direction_t*)
= $directionless_trailing'(true, direction_t*)
def $directionless_trailing'(false, _EMPTY :: direction_t*) = false
def $directionless_trailing'(_, direction_h :: direction_t*)
= $directionless_trailing'(false, direction_t*)
-- if direction_h =/= _EMPTY
The reversed list is walked left to right carrying a boolean flag. The flag
starts as true and flips to false the moment a non-_EMPTY direction is
seen. Any _EMPTY direction encountered after the flag flips signals that a
directionless parameter follows a directional one, which fails the check. The
reversal means “the end” of the original list is seen first.
Extern Declaration
Extern checking is split across the two spec files: ExternMethod_ok lives in
5.12-typing-extern.watsup
and ExternDecl_ok lives in 5.08-typing-declaration.watsup.
ExternMethod_ok
rule ExternMethod_ok:
TC |- functionPrototype ';' : externMethodTypeDefIR
-- if VOID name `( parameterList `) = functionPrototype
-- ParameterList_ok: LOCAL TC |- parameterList : parameterIR* -| TC_body
-- if callableId = $id(name)
-- if externMethodTypeDefIR = VOID callableId `( parameterIR* `)
Each method prototype in an extern block is individually checked by
ExternMethod_ok. Nano-P4 restricts extern methods to return VOID, so the
only information extracted is the method name and its elaborated parameter list.
The result is an externMethodTypeDefIR value:
VOID callableId `( parameterIR* `).
Note that this relation produces externMethodTypeDefIR rather than an updated
typing context. The caller, ExternDecl_ok, is responsible for assembling all
method types into a method environment.
ExternDecl_ok
rule ExternDecl_ok:
TC_0 |- EXTERN name `{ externMethodPrototypeList `} -| TC_1
-- if externMethodPrototype*
= $flatten_externMethodPrototypeList(externMethodPrototypeList)
-- (ExternMethod_ok : TC_0 |- externMethodPrototype : externMethodTypeDefIR)*
-- if (VOID callableId_method `( _ `) = externMethodTypeDefIR)*
-- if $distinct_<callableId>(callableId_method*)
-- if externMethodTypeDefEnv = `{ (callableId_method ':' externMethodTypeDefIR)* `}
-- if typeId = $id(name)
-- if typeDefIR = EXTERN typeId externMethodTypeDefEnv
-- if TC_1 = $add_typeDef_t(TC_0, typeId, typeDefIR)
The rule flattens the method prototype list, checks each prototype independently
under TC_0 via an iteration premise (ExternMethod_ok: ...)*, and collects
the resulting externMethodTypeDefIR*. It then enforces that all method names
are distinct with $distinct_<callableId>, assembles them into a method
environment externMethodTypeDefEnv, and registers the entire extern type in
TC_0’s type definition environment under the extern’s name.
Type Declaration
TypeDecl_ok is a rulegroup with one rule for each kind of type declaration.
Struct and Header
rule TypeDecl_ok/structTypeDeclaration:
TC_0 |- STRUCT name_struct `{ typeFieldList `} -| TC_1
-- if typeField* = $flatten_typeFieldList(typeFieldList)
-- if (type name ';' = typeField)*
-- (Type_ok: TC_0 |- type ~> typeIR)*
-- if (id_field = $id(name))*
-- if $distinct_<id>(id_field*)
-- if typeId = $id(name_struct)
-- if fieldTypeIR* = (typeIR id_field ';')*
-- if typeDefIR = STRUCT typeId `{ fieldTypeIR* `}
-- if TC_1 = $add_typeDef_t(TC_0, typeId, typeDefIR)
The struct rule extracts each field, elaborates each type, checks that field
names are distinct, and registers the resulting struct type definition. The
header rule is identical in structure with HEADER substituted for STRUCT.
Parser, Control, and Package Type Declarations
These three rules follow the same pattern; here is the parser case:
rule TypeDecl_ok/parserTypeDeclaration:
TC_0 |- PARSER name `( parameterList `) ';' -| TC_1
-- ParameterList_ok: BLOCK TC_0 |- parameterList : parameterIR* -| TC_body
-- if $no_object_params(parameterIR*)
-- if typeId = $id(name)
-- if typeDefIR = PARSER typeId `( parameterIR* `)
-- if TC_1 = $add_typeDef_t(TC_0, typeId, typeDefIR)
A parser, control, or package type declaration (as opposed to a full parser or
control definition) is just a signature: a name and a parameter list with no
body. The parameter list is elaborated under BLOCK scope, and
$no_object_params (from Section 3.5) ensures that
none of the parameters have object types.
The type definition registered in TC_1 is
PARSER typeId `( parameterIR* `), CONTROL typeId `( parameterIR* `), or
PACKAGE typeId `( parameterIR* `) depending on the variant. These type
definitions are what the type checker looks up during package instantiation.
Top-Level Declarations
Decl_ok is the dispatch relation for individual top-level declarations. Most
rules simply delegate to one of the specialized relations above. A few are worth
examining directly.
Instantiation
rule Decl_ok/instantiation:
TC_0 |- (_TID typeId_target) `( argumentList `) name ';' -| TC_0
-- if PACKAGE typeId `( parameterIR* `) = $find_typeDef_t(TC_0, typeId_target)
-- ArgumentList_ok: GLOBAL TC_0 |- argumentList : argumentIR*
-- Call_convention_ok: parameterIR* '@' argumentIR*
-- if typeId_object = $id(name)
-- if typeId_object = "main"
-- if packageObjectTypeIR = PACKAGE typeId_object `( parameterIR* `)
-- if varTypeIR = _EMPTY packageObjectTypeIR
-- if TC_1 = $add_var_t(GLOBAL, TC_0, typeId_object, varTypeIR)
Instantiation connects parsers, controls, and externs into the main package.
In Nano-P4, the only instantiable type at the top level is a PACKAGE type. The
rule looks up the target type, checks that the arguments match the package’s
parameters via Call_convention_ok, and verifies that the object name is
literally "main". The resulting variable is registered in the global frame
with direction _EMPTY.
Match-Kind Declaration
rule Decl_ok/matchKindDeclaration:
TC_0 |- MATCH_KIND `{ nameList `} -| TC_1
-- if name* = $flatten_nameList(nameList)
-- if (id = $id(name))*
-- if $distinct_<id>(id*)
-- if varTypeIR* = $repeat_<varTypeIR>(_EMPTY MATCH_KIND, |id*|)
-- if TC_1 = $add_vars_t(GLOBAL, TC_0, id*, varTypeIR*)
A match_kind declaration introduces a set of named constants of type
MATCH_KIND. The rule checks for name collisions with $distinct_<id>, then
creates one _EMPTY MATCH_KIND variable for each name and adds them all to the
global frame using $add_vars_t.
Parser Declaration
rule Decl_ok/parserDeclaration:
TC_0 |- parserDeclaration -| TC_2
-- if PARSER name `( parameterList `)
`{ parserLocalDeclarationList parserStateList `} = parserDeclaration
-- ParameterList_ok: BLOCK TC_0 |- parameterList : parameterIR* -| TC_body
-- ParserLocalDeclList_ok: TC_body |- parserLocalDeclarationList -| TC_1
-- ParserStateList_ok: TC_1 |- parserStateList
-- if callableId = $id(name)
-- if callableTypeDef = PARSER parameterIR*
-- if TC_2 = $add_callableDef_t(TC_0, callableId, callableTypeDef)
A full parser declaration is checked in three stages: the parameter list is
elaborated to produce TC_body, the local declarations are threaded through to
extend it into TC_1, and the parser states are checked under TC_1. Finally,
the callable type PARSER parameterIR* is added to TC_0’s callable
environment.
The parser-specific relations ParserLocalDeclList_ok and ParserStateList_ok
are covered in Section 3.7.
Control Declaration
rule Decl_ok/controlDeclaration:
TC_0 |- controlDeclaration -| TC_2
-- if CONTROL name
`( parameterList `)
`{ controlLocalDeclarationList APPLY controlBody `} = controlDeclaration
-- ParameterList_ok: BLOCK TC_0 |- parameterList : parameterIR* -| TC_body
-- ControlLocalDeclList_ok: TC_body |- controlLocalDeclarationList -| TC_1
-- Block_ok: TC_1 |- controlBody
-- if callableId = $id(name)
-- if callableTypeDef = CONTROL parameterIR*
-- if TC_2 = $add_callableDef_t(TC_0, callableId, callableTypeDef)
A full control declaration follows a similar three-stage structure as a parser
declaration: the parameter list is elaborated to produce TC_body, the
control-local declarations are threaded through to extend TC_body into TC_1,
and the apply block is checked under TC_1 via Block_ok. After all that,
the control’s callable type CONTROL parameterIR* is added to TC_0’s callable
environment.
The control-specific relation ControlLocalDeclList_ok is covered in
Section 3.8.
Sequencing Declarations: Decls_ok and Program_ok
All top-level declarations are sequenced by Decls_ok:
rule Decls_ok/nil:
TC_0 |- eps -| TC_0
rule Decls_ok/cons:
TC_0 |- declaration_h :: declaration_t* -| TC_2
-- Decl_ok: TC_0 |- declaration_h -| TC_1
-- Decls_ok: TC_1 |- declaration_t* -| TC_2
This is the same threading pattern used by Statements_ok in
Section 3.4 and Parameters_ok in
Section 3.5.
Program_ok is the entry point for the entire program:
rule Program_ok:
|- program -| TC'
-- if declaration* = $flatten_program(program)
-- if TC = $empty_typingContext
-- Decls_ok: TC |- declaration* -| TC'
It starts from an empty typing context, flattens the top-level declaration list,
and threads the declarations through Decls_ok. The resulting context TC'
holds all top-level type definitions, callable types, and global variables after
the program has been fully checked.
Exercise
Branch:
exercise/3.6
Check out the exercise branch in the spec submodule:
git -C nano-p4/spec checkout exercise/3.6
Run the following test to observe the failure:
./nano-p4spectec check nano-p4/spec -i nano-p4/include -p nano-p4/testdata/exercise/3.6.p4
The checker will report that directionless_trailing is undefined. The entire
$directionless_trailing definition has been removed from
5.08-typing-declaration.watsup.
Write it from scratch. Use $rev_<direction> to reverse the list, and
$directionless_trailing' as the recursive worker that carries a boolean flag.
There are four tests for this exercise: 3.6.p4 and 3.6.1.p4 are positive
tests (should pass); 3.6.2.p4 and 3.6.3.p4 are negative tests (should fail).
./nano-p4spectec check nano-p4/spec \
-i nano-p4/include \
-p nano-p4/testdata/exercise/3.6.p4
# should pass!
./nano-p4spectec check nano-p4/spec \
-i nano-p4/include \
-p nano-p4/testdata/exercise/3.6.1.p4
# should pass!
./nano-p4spectec check nano-p4/spec \
-i nano-p4/include \
-p nano-p4/testdata/exercise/3.6.2.p4
# should fail!
./nano-p4spectec check nano-p4/spec \
-i nano-p4/include \
-p nano-p4/testdata/exercise/3.6.3.p4
# should fail!
When you are done, restore the original branch:
git -C nano-p4/spec checkout main