Skip to content
code0.HDL

Methods: value, Action, ActionValue

Tell the three method kinds apart by their result type, call them from rules, and see how a method's own readiness condition decides whether the calling rule fires at all.

In this chapter

  • Pick a value, Action or ActionValue method from a transaction's contract and define each kind
  • Trace a rule that calls methods as one firing in which every read sees the entry state
  • Bind the result of an ActionValue with <- and explain why = binds the unexecuted action
  • Explain how an if in a method header withholds the whole firing of a calling rule
Concepts in this chapter
  • value, Action, ActionValue methods
Prerequisite concepts

1. The result type decides the kind of method

The first chapter used one value method. An interface can also offer methods that change the module's state, and the result type in the prototype tells you which kind you are looking at.

Result typeKindChanges stateReturnsHow a rule calls it
Bit#(8), Bool, any value typevalue methodneverthat valueanywhere an expression goes: c.value
ActionAction methodmaynothingas a statement: c.bump;
ActionValue#(t)ActionValue methodmaya twith <-: let x <- t.take;

Pick the kind from the contract. If calling twice must have no effect, it is a value method. If the caller issues a command, it is an Action method. If the returned value and the state change must be inseparable, so that nobody can read one ticket and reserve another, it is an ActionValue method.

One interface often mixes all three. A queue is the standard case:

interface Queue;
  method Action enq(Bit#(8) x);        // command: changes state only
  method ActionValue#(Bit#(8)) take;   // remove and return in one step
  method Bool notEmpty;                // observation: returns a value only
endinterface

Splitting take into a value method plus an Action method would hand the caller the job of making sure that the value it read is the value that got removed.

Check

A module hands out unique ticket numbers: each request must return the current number and advance the counter, so that two requests never get the same number. Which kind of method is request?

Your answer

Form

// value method
method ReturnType name(arguments) = expression;

// Action method
method Action name(arguments);
  statements;
endmethod

// ActionValue method
method ActionValue#(t) name(arguments);
  statements;
  return expression;
endmethod
method Bit#(8) total = sum;

method Action add(Bit#(8) x);
  sum <= sum + x;
endmethod

method ActionValue#(Bit#(8)) drain;
  sum <= 0;
  return sum;
endmethod
DefinitionBehavior
totalReturns the current sum; no state changes
addSchedules the next value of sum; returns nothing
drainSchedules sum to become 0 and, in the same call, returns the value before the update

An Action method body is shorthand for an action block, and an ActionValue method body is shorthand for an actionvalue block; the next sections introduce both. If you repeat types in a method definition, they must match the interface declaration exactly.

Note. return sum in drain returns the old value, not 0. <= only schedules a register's next value; every read inside the same call still sees the value the register had when the call began.

2. action … endaction: describing work

An expression of type Action is a description of work, not the work itself. An action block groups primitive actions, such as register writes and Action method calls, into one atomic action.

Form

action
  statement;
  statement;
endaction
Action clearBoth = action
  left <= 0;
  right <= 0;
endaction;

rule clear (step == 1);
  clearBoth;
endrule
CodeWhat happens
Action clearBoth = action ... endaction;binds a description of two writes; nothing is cleared yet
clearBoth; inside the ruleexecutes the action; both writes commit at the same clock edge

noAction is the built-in action that does nothing. It is useful where an action is required but no state should change.

The rules of an atomic action are the rules of the previous chapter, now applied to every method call as well:

  • Every register read in the action sees the value at the start of the rule.
  • All writes commit together at the clock edge; a later line never reads a value written earlier in the same action.
  • A method called in the action adds its ready condition to the rule's implicit condition. If the method is not ready, the rule does not fire, no matter what its explicit guard says (section 9).

3. A method called from a rule is part of that firing

When a rule calls c.bump, the write inside bump lands at the same edge as the rule's own writes, and every read inside the method sees the start-of-cycle state. The method body is not a procedure that runs first; it is more logic added to the rule's single transition.

The Counter in ActionMethod.bsv has bump, which changes state, and value, which reads it. While step < 3, rule drive prints c.value and then calls c.bump every cycle. When step is 3, rule done prints the final value. Notice that inside drive the print comes before c.bump. Whether that order matters is the question this example answers.

Predict before you run. What does value print on each of the three firings of drive? What final value does done print?

Check

What does value print on the first firing of drive?

ActionMethod.bsv
package ActionMethod;

// An Action method changes state and returns nothing.
interface Counter;
  method Action bump;
  method Bit#(8) value;
endinterface

module mkCounter(Counter);
  Reg#(Bit#(8)) count <- mkReg(0);

  method Action bump;
    count <= count + 1;
  endmethod

  method Bit#(8) value = count;
endmodule

(* synthesize *)
module mkTop(Empty);
  Counter c <- mkCounter;
  Reg#(Bit#(4)) step <- mkReg(0);

  rule drive (step < 3);
    // The value method observes the state at the start of the rule, so this
    // prints the count before bump takes effect.
    $display("step %0d: value = %0d", step, c.value);
    c.bump;
    step <= step + 1;
  endrule

  rule done (step == 3);
    $display("final value = %0d", c.value);
    $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result. Tracing the state cycle by cycle:

Cyclecount at startRule that firesvalue readscount after edge
00drive01
11drive12
22drive23
33done33

The three step lines show 0, 1, and 2. c.value inside drive cannot see the write that c.bump scheduled in the same rule; it reads the state at the start of the cycle. The final 3 is what done sees in the cycle after the third write lands.

Try it yourself.

Check

Move the $display line below c.bump; in drive. What changes?

Your answer

Then change count + 1 in bump to count + 2. The step lines become 0, 2, and 4, and the final line prints 6.

4. actionvalue and <-

ActionValue#(t) describes an atomic action that also returns a value of type t. Inside an actionvalue block, return gives the result.

Form

actionvalue
  statements;
  return expression;
endactionvalue

Type name <- actionValueCall;
let name <- actionValueCall;
method ActionValue#(UInt#(8)) issue;
  next <= next + 1;
  return next;
endmethod

let ticket <- dispenser.issue;

<- performs the action on the right and binds its result. It is the same operator used for module instantiation, because instantiating a module is also a computation that you run to get a result, the interface. With =, you would bind the unexecuted expression instead, and a declaration such as UInt#(8) ticket = dispenser.issue; fails with T0020: expected UInt#(8), inferred ActionValue#(UInt#(8)).

5. An ActionValue returns the entry value and schedules the next

In ActionValueMethod.bsv, take reserves the next ticket number and returns the current one: it returns next and writes next + 1 in the same call. The caller, drive, receives the result with let issued <- t.take;.

Predict before you run. For each call, write a pair: the value returned now, and the value next holds in the following cycle.

Check

next starts at 1. What is the first issued value?

ActionValueMethod.bsv
package ActionValueMethod;

// An ActionValue method does both: it changes state and returns a value.
interface Ticket;
  method ActionValue#(Bit#(8)) take;
endinterface

module mkTicket(Ticket);
  Reg#(Bit#(8)) next <- mkReg(1);

  method ActionValue#(Bit#(8)) take;
    next <= next + 1;
    return next;
  endmethod
endmodule

(* synthesize *)
module mkTop(Empty);
  Ticket t <- mkTicket;
  Reg#(Bit#(4)) step <- mkReg(0);

  rule drive (step < 3);
    // Binding the result of an ActionValue method uses <-, the same operator
    // module instantiation uses.
    let issued <- t.take;
    $display("issued %0d", issued);
    step <= step + 1;
  endrule

  rule done (step == 3);
    $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result.

CallReturnednext in the following cycle
1st12
2nd23
3rd34

Three issued lines reading 1, 2, and 3 confirm that take returns the value from before its update. Returning and reserving in one method call prevents clients from receiving the same number before the 8-bit counter wraps. After 256 accepted calls, numbers repeat.

Try it yourself.

ChangeExpected result
mkReg(1) to mkReg(5)The issued lines read 5, 6, 7
let issued <- t.take; to Bit#(8) issued = t.take;Compile error T0020: expected Bit#(8), inferred ActionValue#(Bit#(8))

Caution. Bit#(8) issued = t.take; is a type error because = binds the unexecuted action, whose type is ActionValue#(Bit#(8)), not a Bit#(8). Only <- runs it.

6. Example: issue the current ticket

ActionValueTicket.bsv has a dispenser whose issue method returns a ticket number and advances the counter, written with an explicit actionvalue … endactionvalue block. Read the TicketDispenser interface, then the next register and issue method, and finally the issueTwo rule and its if.

The state starts at next = 40 and issued = 0.

FiringEntry state (next, issued)ReturnedScheduled next state
first(40, 0)40(41, 1)
second(41, 1)41(42, 2), and $finish

The if (issued == 1) in the second firing also reads the entry value, so the simulation ends right after the second ticket is printed.

Predict before you run. Does return next; see the value scheduled by next <= next + 1; on the line above it?

Check

Which two ticket numbers are printed, in which order?

Your answer

ActionValueTicket.bsv
package ActionValueTicket;

interface TicketDispenser;
  method ActionValue#(UInt#(8)) issue;
endinterface

module mkTicketDispenser(TicketDispenser);
  Reg#(UInt#(8)) next <- mkReg(40);

  method ActionValue#(UInt#(8)) issue;
    actionvalue
      next <= next + 1;
      // The returned value is read before the scheduled register update commits.
      return next;
    endactionvalue
  endmethod
endmodule

(* synthesize *)
module mkTop(Empty);
  TicketDispenser dispenser <- mkTicketDispenser;
  Reg#(UInt#(2)) issued <- mkReg(0);

  rule issueTwo (issued < 2);
    let ticket <- dispenser.issue;
    $display("ticket=%0d", ticket);
    issued <= issued + 1;
    if (issued == 1) $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result.

Printed lineWhat it proves
first ticket is 40return next read the entry state, not the scheduled 41
second ticket is 41the scheduled write committed at the edge before the second firing

The returned value and the scheduled next state belong to one atomic action, but they are values from two different moments.

Try it yourself.

  1. Change the contract to "advance, then report": replace return next; with return next + 1; and add no second write. The output becomes 41 and 42, while the register still steps 40, 41, 42. Only the value contract changed.
  2. Move return next; above next <= next + 1; in your head. Nothing changes: textual order inside an action does not decide what a read sees.

7. Rules inside modules

Rules describe a module's internal behavior. A rule has a name, an optional explicit condition, and an action body, and the body may call the methods of the module's own child instances.

Form

rule name (explicitCondition);
  actions;
endrule
rule drain (step == 3 && stack.notEmpty);
  let value <- stack.pop;
  $display("%0d", value);
endrule
PartMeaning
drainA rule name, unique within the module
(step == 3 && stack.notEmpty)The explicit condition; leaving it out means always true
stack.popA method call; pop's implicit condition joins the rule's enabling condition

When a rule fires, every state update in its body takes effect as one atomic transition. Even when the calls reach state in several child modules, no one can observe a state where only part of the update happened. bsc generates the arbitration and multiplexers needed when several rules update the same state.

8. Interfaces with type parameters and subinterfaces

8.1 Interfaces with type parameters

Adding #(type name) after the interface name leaves the element type open.

Form

interface InterfaceName#(type typeVariable);
  ...
endinterface
interface Holder#(type t);
  method Action put(t x);
  method t get;
endinterface

Holder#(Bit#(8)) small;   // an interface whose element is Bit#(8)
Holder#(Bool)    flag;    // a different interface type, element Bool

Each use fixes the parameter to one concrete type, so Holder#(Bit#(8)) and Holder#(Bool) are distinct types.

Note. If a parameter stands for a size and its use does not make that clear, declare it as numeric type depth.

8.2 Subinterfaces and Empty

An interface may contain another interface as a member. Write interface before the member; clients reach it with dot notation.

interface Channel;
  interface Holder#(Bit#(8)) data;
  method Action clear;
endinterface

// a client
ch.data.put(8'h2a);

Building large contracts from small ones lets several modules reuse the same pieces. Clock and Reset are interface types too, so they can appear as subinterfaces.

The Prelude's Empty interface has no methods at all. A top-level module that offers nothing to the outside is written module mkTop(Empty);.

9. A method can refuse: the implicit condition

A method definition may carry if (condition) in its header. That condition is the method's readiness, also called its implicit condition. In a cycle where it is false the method is not ready, and any rule that calls it does not fire at all, whatever the rule's own guard says. The rule is not partly executed with the call skipped; the whole firing is withheld.

Form

method Action push(Bit#(8) x) if (depth < 2);
  stored <= x;
  depth  <= depth + 1;
endmethod

The guard of a calling rule and the readiness of every method it calls are combined: a rule is ready only when all of them are true. Readiness belongs on the method, where the invariant lives, so that every caller is protected without repeating the check.

Caution. An if in the method body is not the same thing. if (depth < 2) begin … end inside push leaves the method always ready; the calling rule keeps firing and only the writes are skipped. Readiness lives in the header.

The previous chapter separated a rule being ready from its firing. Methods add a third fact to check when a rule seems stuck: is every method it calls ready? The order to debug in is readiness of the called methods, then the rule's own guard, then conflicts with other rules. A failure at any stage looks the same from outside: nothing changed. The next chapter runs a rule with no guard of its own that stops when push stops being ready, and states the rules for what a readiness condition may mention.

10. Apply it: one transition per call

Four Practice problems. In the graded one, bump writes count <= ?; and the counter ends at an arbitrary value; replace the don't-care with the next value derived from the current count, so that each call is exactly one increment. In the second, step(clear, enable) is one Action method that takes both commands as Bool arguments; write its body so that clear wins, enable counts and neither holds, as the design chapter works out from the requirement. In the last two, take and issue each return next + 1 and every issued ticket is one ahead; keep the write and change only the returned expression to the entry value.

11. Common mistakes

11.1 Binding an ActionValue result with =

// Wrong: binds the action itself, not its result
Bit#(8) issued = t.take;

// Right
Bit#(8) issued <- t.take;

= binds the right-hand expression without performing it. With a declared type, error T0020 shows the expected type Bit#(8) beside the inferred ActionValue#(Bit#(8)); the error is a sign that the code mixed up the description of an action with its result. With let issued = t.take; the mistake surfaces later, because issued becomes the action itself: using it in arithmetic such as issued + 1 fails with T0031, which reports no instance of Arith#(ActionValue#(Bit#(8))).

11.2 Returning the scheduled value instead of the current one

// Wrong for "issue then advance": returns the next ticket
next <= next + 1;
return next + 1;

// Right: return the entry state
next <= next + 1;
return next;

Both versions compile. If returned values are consistently one step ahead, compare the return expression with the entry state of the register.

11.3 Updating only part of the state in pop

// Wrong: only depth changes
method ActionValue#(t) pop if (depth > 0);
  depth <= depth - 1;
  return items[0];
endmethod

// Right: the stored elements move too
method ActionValue#(t) pop if (depth > 0);
  items <= shiftInAtN(items, ?);
  depth <= depth - 1;
  return items[0];
endmethod

The wrong version still typechecks. When a method keeps returning the same element, check that it updates every piece of state it is responsible for. The stack this pop belongs to is the closing example of the next chapter.

12. Summary

ElementMeaning
value methodreads; called in expressions; never changes state
Action methodchanges state; called as a statement in a rule
ActionValue#(t) methodchanges state and returns a t; called with <-
action … endactionatomic work; binding it with = does not execute it; noAction does nothing
actionvalue … endactionvalueatomic work plus a result read from the entry state; execute with <-
<-performs a module computation or action and binds its result; = binds an expression without performing it
inside a callreads see the entry state; writes land with the rule's
if (cond) in a method headerreadiness; false means the calling rule does not fire
ready vs firedmethod readiness, the rule's guard and conflicts are three separate checks
Interface#(type t), subinterfaces, Emptyan open element type fixed at each use; interfaces nested with dot access; the interface with no methods

13. Check your understanding

  1. A queue must remove an item and return it. Which kind of method fits?

    Show answer

    An ActionValue method. Returning and removing in one call guarantees that the value removed is the value returned.

  2. Inside one rule, if you read c.value on the line after c.bump;, do you see the updated value?

    Show answer

    No. Every call in a rule reads the register's state at the start of the cycle. The value bump scheduled appears in the next cycle.

  3. Why does Action clearBoth = action ... endaction; not clear any register?

    Show answer

    = only binds the description of the work to a name. The writes happen when clearBoth is executed as a statement in an action context such as a rule body.

  4. An ActionValue method writes next <= next + 1 and returns next. Which value does the caller receive?

    Show answer

    The value at the start of the firing. The write commits at the clock edge, so the incremented value is seen only by the next firing.

The next chapter follows a method's readiness out to its callers, adds parameters and arguments to module definitions, and sets the conditions an interface must meet at a synthesis boundary.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Sources

Licences and notices for the sources this site follows