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 type | Kind | Changes state | Returns | How a rule calls it |
|---|---|---|---|---|
Bit#(8), Bool, any value type | value method | never | that value | anywhere an expression goes: c.value |
Action | Action method | may | nothing | as a statement: c.bump; |
ActionValue#(t) | ActionValue method | may | a t | with <-: 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
endinterfaceSplitting 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?
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;
endmethodmethod Bit#(8) total = sum;
method Action add(Bit#(8) x);
sum <= sum + x;
endmethod
method ActionValue#(Bit#(8)) drain;
sum <= 0;
return sum;
endmethod| Definition | Behavior |
|---|---|
total | Returns the current sum; no state changes |
add | Schedules the next value of sum; returns nothing |
drain | Schedules 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;
endactionAction clearBoth = action
left <= 0;
right <= 0;
endaction;
rule clear (step == 1);
clearBoth;
endrule| Code | What happens |
|---|---|
Action clearBoth = action ... endaction; | binds a description of two writes; nothing is cleared yet |
clearBoth; inside the rule | executes 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?
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
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result. Tracing the state cycle by cycle:
| Cycle | count at start | Rule that fires | value reads | count after edge |
|---|---|---|---|---|
| 0 | 0 | drive | 0 | 1 |
| 1 | 1 | drive | 1 | 2 |
| 2 | 2 | drive | 2 | 3 |
| 3 | 3 | done | 3 | 3 |
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?
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?
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
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Call | Returned | next in the following cycle |
|---|---|---|
| 1st | 1 | 2 |
| 2nd | 2 | 3 |
| 3rd | 3 | 4 |
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.
| Change | Expected 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.
| Firing | Entry state (next, issued) | Returned | Scheduled 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?
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
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Printed line | What it proves |
|---|---|
first ticket is 40 | return next read the entry state, not the scheduled 41 |
second ticket is 41 | the 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.
- Change the contract to "advance, then report": replace
return next;withreturn 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. - Move
return next;abovenext <= 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;
endrulerule drain (step == 3 && stack.notEmpty);
let value <- stack.pop;
$display("%0d", value);
endrule| Part | Meaning |
|---|---|
drain | A rule name, unique within the module |
(step == 3 && stack.notEmpty) | The explicit condition; leaving it out means always true |
stack.pop | A 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);
...
endinterfaceinterface 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 BoolEach 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;
endmethodThe 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];
endmethodThe 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
| Element | Meaning |
|---|---|
| value method | reads; called in expressions; never changes state |
Action method | changes state; called as a statement in a rule |
ActionValue#(t) method | changes state and returns a t; called with <- |
action … endaction | atomic work; binding it with = does not execute it; noAction does nothing |
actionvalue … endactionvalue | atomic 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 call | reads see the entry state; writes land with the rule's |
if (cond) in a method header | readiness; false means the calling rule does not fire |
| ready vs fired | method readiness, the rule's guard and conflicts are three separate checks |
Interface#(type t), subinterfaces, Empty | an open element type fixed at each use; interfaces nested with dot access; the interface with no methods |
13. Check your understanding
-
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.
-
Inside one rule, if you read
c.valueon the line afterc.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
bumpscheduled appears in the next cycle. -
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 whenclearBothis executed as a statement in an action context such as a rule body. -
An ActionValue method writes
next <= next + 1and returnsnext. 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.