Evaluation Context
Before writing a single evaluation rule, we need somewhere to store what the
interpreter knows at runtime: which type names exist, which callables have been
loaded, and which variables are currently in scope along with their runtime
values. That storage is the evaluation context, defined in
8.00-eval-context.watsup.
If you have read Section 3.1, the structure here will look familiar. The evaluation context mirrors the three-layer design of the typing context, but maps names to values rather than types.
The Three Layers
syntax evalContext =
{ GLOBAL globalEvalLayer,
BLOCK blockEvalLayer,
LOCAL localEvalLayer }
Global layer holds information visible everywhere: the type definition environment, the callable definitions loaded by the loading phase, a global frame, the loaded parser declaration, and the loaded control declaration.
syntax globalEvalLayer =
{ TYPE typeDefEnv,
CALLABLE callableDefEnv,
FRAME frame,
PARSER parserDeclarationIR,
CONTROL controlDeclarationIR }
The PARSER and CONTROL fields are new compared to the typing context. At
runtime the interpreter needs the full bodies of the top-level parser and
control, not merely their type signatures, so the evaluation context carries the
complete IR forms.
Block layer holds runtime values for the parameters and block-level declarations of the current parser or control block.
syntax blockEvalLayer =
{ FRAME frame }
Local layer holds variables declared inside a block body, as a stack of frames.
syntax localEvalLayer =
{ FRAMES frame* }
A frame is a map from nameIR to value:
syntax frame = map<nameIR, value>
Unlike the typing context, which stores a (direction, type) pair per name, a
frame here stores the actual runtime value. Direction is irrelevant at
evaluation time; what matters is the current contents of each variable.
Constructing the Initial Context
$make_evalContext builds the evaluation context from the typing context and
load context produced by the loading phase:
def $make_evalContext(TC, LC) = EC
-- if parserDeclarationIR = LC.PARSER
-- if controlDeclarationIR = LC.CONTROL
-- if globalEvalLayer
= { TYPE TC.GLOBAL.TYPE,
CALLABLE LC.CALLABLE,
FRAME $empty_frame,
PARSER parserDeclarationIR,
CONTROL controlDeclarationIR }
-- if blockEvalLayer = { FRAME $empty_frame }
-- if localEvalLayer = { FRAMES ([ $empty_frame ]) }
-- if EC
= { GLOBAL globalEvalLayer,
BLOCK blockEvalLayer,
LOCAL localEvalLayer }
The type definitions come from the typing context (TC.GLOBAL.TYPE), while the
callable definitions and loaded declarations come from the load context (LC).
All three frames start empty; the interpreter fills them as it evaluates
variable declarations and calls.
Context Inheritance
When the interpreter enters a new callable, it does not start with a blank
slate. The global layer should still be accessible, but the block and local
layers must be reset so that the new callable’s parameters and locals do not
collide with the caller’s. $inherit_e handles this reset:
def $inherit_e(GLOBAL, EC)
= EC[ .BLOCK = blockEvalLayer ][ .LOCAL = localEvalLayer ]
-- if blockEvalLayer = { FRAME $empty_frame }
-- if localEvalLayer = { FRAMES ([ $empty_frame ]) }
def $inherit_e(BLOCK, EC)
= EC[ .LOCAL = localEvalLayer ]
-- if localEvalLayer = { FRAMES ([ $empty_frame ]) }
def $inherit_e(LOCAL, EC) = EC
The three clauses form a staircase: inheriting at GLOBAL scope resets both the
block frame and the local stack; inheriting at BLOCK scope resets only the
local stack; inheriting at LOCAL scope leaves everything untouched.
There is no counterpart to $inherit_e in the static semantics. Type checking
walks each top-level declaration under the outer context and never invokes a
callable body from an arbitrary caller, so it never needs to reset the block and
local layers the way the interpreter does at a call site.
Helper Functions
The rest of 8.00-eval-context.watsup defines helper functions that closely
mirror those in 5.00-typing-context.watsup from
Section 3.1, so they are not covered in full
here. Briefly:
- Finders (
$find_var_e,$find_callableDef_e,$find_typeDef_e): look up a name by walking the same local-then-block-then-global scope chain as the typing context finders. - Adders (
$add_var_e): insert a new binding into the appropriate layer, refusing to overwrite an existing name. - Frame entry and exit (
$enter_e,$exit_e): push and pop a fresh frame on the local stack when entering and leaving a block statement, the runtime counterpart to$enter_t/$exit_t.
One helper with no direct analog in the typing context is the updater
($update_var_e). The type checker never changes a variable’s type after
declaration, but the interpreter must overwrite values on every assignment. The
local updater walks the frame stack recursively until it finds the frame that
owns the name:
def $update_var_e(LOCAL, EC, nameIR, value)
= EC_pop'[ .LOCAL.FRAMES = frame_h :: EC_pop'.LOCAL.FRAMES ]
-- if frame_h :: frame_t* = EC.LOCAL.FRAMES
-- if ~$in_set<nameIR>($dom_map<nameIR, value>(frame_h), nameIR)
-- if EC_pop = EC[ .LOCAL.FRAMES = frame_t* ]
-- if EC_pop' = $update_var_e(LOCAL, EC_pop, nameIR, value)
This clause fires when the name is not in the head frame: it pops the head, recurses on the tail, then pushes the head back on top of the updated context. Without it, assigning to a variable declared in an outer scope from inside a nested block would fail.
The EC Meta-variable
var EC : evalContext
EC is the typed meta-variable for the evaluation context, analogous to TC in
the static semantics. Any identifier beginning with EC (EC', EC_0,
EC_pop, etc.) is recognized by the elaborator as a value of type
evalContext. You will see it threaded through almost every evaluation relation
in the chapters that follow.
Exercise
Branch:
exercise/5.1
Check out the exercise branch in the spec submodule:
git -C nano-p4/spec checkout exercise/5.1
Run the following test to observe the failure:
./nano-p4spectec eval nano-p4/spec -i nano-p4/include -p nano-p4/testdata/exercise/5.1.p4 -stf nano-p4/testdata/exercise/5.1.stf
The test should succeed, but the interpreter gets stuck. If you get stuck,
Chapter 6 covers the debugging techniques that apply here. In
8.00-eval-context.watsup, the LOCAL updater clause that handles the case
where a name is not in the head frame has been removed. Without it, assigning to
a variable declared in an outer scope from inside a nested block fails silently.
Add the missing clause back to $update_var_e(LOCAL, ...) in
8.00-eval-context.watsup.
When you are done, restore the original branch:
git -C nano-p4/spec checkout main