Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Expressions and L-values

8.03-eval-expression.watsup and 8.04-eval-lvalue.watsup define the Expr_eval, Lvalue_eval, and Lvalue_write relations.

relation Expr_eval:
  scope evalContext |- expression : value
  hint(input %0 %1 %2)

Read scope EC |- e : v as: “under context EC at scope scope, expression e evaluates to value v.”

The structure mirrors Expr_ok from Section 3.3 almost rule-for-rule with one key difference: where Expr_ok produces a typeIR, Expr_eval produces a value.

Literals

Boolean and integer literals evaluate to themselves:

rule Expr_eval/true:
  scope EC |- TRUE : _B true

rule Expr_eval/false:
  scope EC |- FALSE : _B false

rule Expr_eval/integerLiteral:
  scope EC |- integerLiteral : integerLiteral

Integer literals are already values in the syntax (w W i or w S i), so the rule is a no-op: the literal on the right of |- is identical to the one on the left.

Reference Expressions

rule Expr_eval/referenceExpression:
  scope EC |- name : value
  -- if nameIR = $id(name)
  -- if value = $find_var_e(scope, EC, nameIR)

The rule converts name to nameIR and looks up the current value in the evaluation context. Compare with Expr_ok/referenceExpression, which calls $find_var_t to get a type instead.

Unary and Binary Expressions

rule Expr_eval/unaryExpression:
  scope EC |- unop expression : value'
  -- Expr_eval: scope EC |- expression : value
  -- if value' = $un_op(unop, value)

rule Expr_eval/binaryExpression:
  scope EC |- expression_l binop expression_r : value
  -- Expr_eval: scope EC |- expression_l : value_l
  -- Expr_eval: scope EC |- expression_r : value_r
  -- if value = $bin_op(binop, value_l, value_r)

Both rules delegate the actual computation to helpers defined in 3.1-operations.watsup: $un_op for unary operators and $bin_op for binary operators. These helpers are defined exhaustively for every operator and value type combination. For example, unsigned addition converts both operands to raw integers, adds them, and wraps the result back into a fixed-width bitstring:

def $bin_op('+', w W i_l, w W i_r) = w W i'
  -- if i_l' = $bitstr_to_int(w, i_l)
  -- if i_r' = $bitstr_to_int(w, i_r)
  -- if i'   = $int_to_bitstr(w, $(i_l' + i_r'))

The static semantics already ensures that both operands have the same type, so $bin_op never has to handle mismatched widths at runtime.

Member Access

rule Expr_eval/struct:
  scope EC |- memberAccessBase '.' member : value_member
  -- Expr_eval: scope EC |- memberAccessBase : value_base
  -- if STRUCT typeId `{ fieldValue* `} = value_base
  -- if (value_field nameIR_field ';' = fieldValue)*
  -- if nameIR = $id(member)
  -- if value_member
      = $assoc_<nameIR, value>(nameIR, (nameIR_field, value_field)*)

rule Expr_eval/header:
  scope EC |- memberAccessBase '.' member : value_member
  -- Expr_eval: scope EC |- memberAccessBase : value_base
  -- if HEADER typeId `{ fieldValue* `} = value_base
  -- if (value_field nameIR_field ';' = fieldValue)*
  -- if nameIR = $id(member)
  -- if value_member
      = $assoc_<nameIR, value>(nameIR, (nameIR_field, value_field)*)

Where Expr_ok/struct destructures the type STRUCT _ `{ (typeIR id ';')* `} to find the field’s type, Expr_eval/struct destructures the value STRUCT typeId `{ fieldValue* `} to find the field’s current value. The $assoc_ call performs a linear scan of the (nameIR_field, value_field)* pairs to return the value associated with nameIR.

Parenthesized Expressions

rule Expr_eval/parenthesizedExpression:
  scope EC |- `( expression `) : value
  -- Expr_eval: scope EC |- expression : value

Parentheses are transparent at runtime, just as they are during type checking.

Reading L-values: Lvalue_eval

relation Lvalue_eval:
  scope evalContext |- lvalue : value
  hint(input %0 %1 %2)

An l-value is a location that can appear on the left of an assignment. Reading one is straightforward: convert it to the equivalent expression and delegate to Expr_eval:

rule Lvalue_eval:
  scope EC |- lvalue : value
  -- if expression = $expression_of_lvalue(lvalue)
  -- Expr_eval:
      scope EC |- expression : value

$expression_of_lvalue is a syntactic helper that turns a lvalue into an expression of the same shape. Every l-value form has a corresponding expression form, so the conversion is always total.

Writing L-values: Lvalue_write

Writing is more involved: the interpreter finds the variable that owns the location, updates its stored value, and threads the updated context forward.

relation Lvalue_write:
  scope evalContext |- lvalue := value -| evalContext
  hint(input %0 %1 %2 %3)

Simple Variable

rule Lvalue_write/referenceExpression:
  scope EC_0 |- referenceExpression := value -| EC_1
  -- if nameIR = $id(referenceExpression)
  -- if EC_1 = $update_var_e(scope, EC_0, nameIR, value)

A bare name resolves to a nameIR and calls $update_var_e, which walks the frame stack to find the frame that owns nameIR and overwrites the binding there. The frame walk was covered in Section 5.1.

Member Access

Both rules for struct and header member access follow the same read-modify-write pattern: read the current value of the base l-value, update the relevant field inside it, reconstruct the whole-aggregate value, and write it back recursively. The recursion handles nested member access naturally. These rules are left as the exercise for this section.

$update_fieldValue is a small helper that scans the field list and replaces the value associated with the given nameIR:

dec $update_fieldValue(fieldValue*, nameIR, value) : fieldValue*

def $update_fieldValue(eps, nameIR, value) = eps
def $update_fieldValue(
    (value_field_h nameIR_field_h ';') :: fieldValue_t*,
    nameIR, value)
  = (value nameIR ';') :: fieldValue_t*
  -- if nameIR_field_h = nameIR
def $update_fieldValue(
    (value_field_h nameIR_field_h ';') :: fieldValue_t*,
    nameIR, value)
  = (value_field_h nameIR_field_h ';') ::
      $update_fieldValue(fieldValue_t*, nameIR, value)
  -- if nameIR_field_h =/= nameIR

The first clause handles an empty list. The second replaces the head when its name matches. The third skips the head and recurses when names differ.

Parenthesized L-value

rule Lvalue_write/parenthesized:
  scope EC_0 |- (`( lvalue `)) := value -| EC_1
  -- Lvalue_write:
      scope EC_0 |- lvalue := value -| EC_1

Parentheses are transparent for writing, just as they are for reading.

Exercise

Branch: exercise/5.3

Check out the exercise branch in the spec submodule:

git -C nano-p4/spec checkout exercise/5.3

Run the following test to observe the failure:

./nano-p4spectec eval nano-p4/spec -i nano-p4/include -p nano-p4/testdata/exercise/5.3.p4 -stf nano-p4/testdata/exercise/5.3.stf

The test program writes to a nested member l-value and reads the result back. It should pass, but the interpreter gets stuck. Two rules have been omitted from 8.04-eval-lvalue.watsup. Write them by analogy with each other and with Lvalue_write/referenceExpression.

When you are done, restore the original branch:

git -C nano-p4/spec checkout main