Attributes let you write down facts that ordinary BSV types and rule bodies cannot express: where generated RTL must keep a module boundary, what the generated ports are called, which handshake signals exist, and which rule schedules are acceptable. Some attributes are requests the compiler carries out; others are claims it must prove, or claims it simply trusts.
You need modules, interfaces, atomic rules, method guards, and implicit
conditions, and the three compiler phases from
Static and dynamic semantics.
FIFO and FIFOF from FIFOs, split, and the library primitives
are useful mental examples, because their readiness changes with stored state.
Every example compiles with bsc, generates Verilog, and simulates it with
Icarus Verilog. Run performs all the steps in order.
1. Attribute syntax
1.1 Writing an attribute
An attribute instance sits between (* and *). It holds one or more
comma-separated names, and a name may take a value after =. The attribute
applies to the construct that immediately follows it.
Syntax
(* name *)
(* name = value *)
(* name1, name2 = value *)(* synthesize, clock_prefix = "CORE_CLK" *)
module mkCore(CoreIfc);
...
endmodule| Placement | Governs | Examples |
|---|---|---|
before module | the whole module | synthesize, clock_prefix |
before rule | that rule | fire_when_enabled, descending_urgency |
| before a method in an interface | that method's ports | result, always_ready |
before if or case inside a rule | that conditional | split, nosplit |
before a top-level function | that function | noinline |
Note. An attribute is not a comment and does nothing while the simulation runs. It changes what the compiler checks or generates.
1.2 Four kinds of attribute
What an attribute does depends on the compiler phase it addresses.
| Kind | Examples | Phase | What happens |
|---|---|---|---|
| Generation request | synthesize, noinline, port, clock, and reset names | elaboration and code generation | sets RTL boundaries and names |
| Scheduling choice | descending_urgency, execution_order, preempts | scheduling | limits which enabled rules are chosen, and in what order |
| Proved assertion | always_ready, always_enabled, fire_when_enabled, no_implicit_conditions | static checking | the compiler must prove the fact; if it cannot, compilation fails |
| Trusted assertion | mutually_exclusive, conflict_free | scheduling and simulation | the scheduler believes the claim, and simulation only checks it on the paths you exercise |
Note. The last kind is the dangerous one. A simulation that never violates a trusted claim does not prove the claim for all input sequences, yet the synthesized hardware follows the schedule built on it.
2. Module and function boundaries
2.1 synthesize keeps a module separate
(* synthesize *) asks bsc to generate a separate Verilog module for a BSV
module. Instances elsewhere refer to that generated definition instead of
being inlined into their parent.
Syntax
(* synthesize *)
module mkName(InterfaceType);
...
endmodule(* synthesize *)
module mkCounter(Counter);
Reg#(UInt#(8)) count <- mkReg(0);
method Action incr;
count <= count + 1;
endmethod
method UInt#(8) value = count;
endmoduleThe method results stay the same; only the hierarchy changes. A separate boundary can still change scheduling, as Readiness, module arguments, and synthesis boundaries introduced. Each method argument becomes a single hardware port that carries one value per cycle, so two callers that want different arguments in the same cycle conflict. An inlined module could give each caller its own logic. Each generated module also has its own scheduler, so rule relationships cannot be stated across the boundary.
2.2 noinline does the same for a function
(* noinline *) before a top-level function asks bsc to generate the
function as its own Verilog module rather than expanding it at every call.
Syntax
(* noinline *)
function ResultType name(ArgType arg);
...
endfunction(* noinline *)
function Bit#(1) parity8(Bit#(8) x);
return ^x;
endfunctionCompiling a design that calls parity8 also writes module_parity8.v. The
arguments and the result become ports, so each must have a Bits instance. A
boundary can reduce compile work and keep the structure visible, at the cost
of extra wiring.
2.3 Example: an incrementer behind a boundary
ModuleBoundary.bsv defines an Incrementer whose load method stores its
argument plus one. Read the Incrementer interface first, then find both
synthesize attributes, then follow step through loadValue and
showResult. The command generates mkIncrementer and mkTop separately.
| Cycle start | Rule that fires | State committed at cycle end |
|---|---|---|
step == 0, valueReg == 0 | loadValue calls load(8'h2a) | valueReg == 8'h2b, step == 1 |
step == 1, valueReg == 8'h2b | showResult reads result | value printed, simulation ends |
Predict before you run.
- How many Verilog files does the compile step generate, and what are they called?
- What decimal value does
showResultprint afterload(8'h2a)? - Which construct creates the one-cycle gap between loading and reading:
synthesizeorstep?
Check
Both mkIncrementer and mkTop carry (* synthesize *), and the command generates both. How many Verilog modules does the compile step write?
package ModuleBoundary;
interface Incrementer;
method Action load(Bit#(8) value);
method Bit#(8) result;
endinterface
// Preserve this module as a generated RTL boundary.
(* synthesize *)
module mkIncrementer(Incrementer);
Reg#(Bit#(8)) valueReg <- mkReg(0);
method Action load(Bit#(8) value);
valueReg <= value + 1;
endmethod
method Bit#(8) result = valueReg;
endmodule
(* synthesize *)
module mkTop(Empty);
Incrementer incrementer <- mkIncrementer;
Reg#(Bit#(2)) step <- mkReg(0);
rule loadValue (step == 0);
incrementer.load(8'h2a);
step <= 1;
endrule
rule showResult (step == 1);
$display("incremented = %0d", incrementer.result);
$finish(0);
endrule
endmodule
endpackageCheck the result.
| Observation | What it proves |
|---|---|
incremented = 43 | 8'h2a is 42; the method added one, so the behavior is correct |
mkIncrementer.v and mkTop.v created | the child kept its own generated boundary |
The one-cycle gap comes from step, not from synthesize. The number checks
the behavior; the two files check the boundary.
Try it yourself.
- Remove the child's
synthesizeattribute and drop-g mkIncrementerfrom the command so that onlymkTopis generated. The simulation still prints 43, but onlymkTop.vis created. The incrementer's register and logic are now inlined into the parent. The behavior stayed; the implementation boundary disappeared.
3. Port naming and protocol attributes
3.1 Ready and Enable
A synthesized method can have data ports plus two control signals. They come from different sides of the call.
| Signal | Driven by | Meaning |
|---|---|---|
Ready (RDY_name) | the module that implements the method | it can serve a call this cycle; this is the method's implicit condition |
Enable (EN_name) | the caller | an Action method is being called this cycle |
A value method has a result and may have Ready, but it has no Enable because there is no action to request.
3.2 Renaming generated ports
Naming attributes change only the spelling of generated RTL ports. BSV callers keep using the same method names.
Syntax
interface Name;
(* result = "PORT", ready = "PORT_RDY" *)
method Type valueMethod;
(* enable = "PORT_EN", prefix = "" *)
method Action actionMethod((* port = "PORT_IN" *) Type arg);
endinterface| Attribute | Names |
|---|---|
result | the result port of a value method |
ready | the Ready port |
enable | the Enable port |
port | one method argument port |
prefix | the prefix put in front of argument ports; "" removes it |
3.3 always_ready and always_enabled
Two attributes remove a control port, and in exchange make a claim the compiler must prove.
Syntax
(* always_ready *) method Type name; // no Ready port
(* always_enabled *) method Action name(...); // no Enable portinterface Tick;
(* always_enabled *)
method Action tick(Bit#(1) pulse);
(* always_ready *)
method UInt#(8) count;
endinterface| Attribute | Port removed | Claim that must hold |
|---|---|---|
always_ready | Ready | the method is ready in every cycle |
always_enabled | Enable | the method is called in every cycle; it also implies always_ready |
If the method has a guard that depends on state, the claim is false and compilation fails. The attribute never hides a guard.
3.4 Example: a counter with renamed ports
PortProtocol.bsv defines a NamedCounter with a value method value and an
Action method load. Read the interface before the implementation. The
attributes rename value's result port to COUNT, claim value is always
ready, and rename load's Enable to LOAD.
In cycle 0, loadValue presents 8'h3c and selects the action through the
Enable now called LOAD; count == 8'h3c commits at the edge. In cycle 1,
showValue reads the committed value through the port called COUNT.
Predict before you run.
- Which identifiers does
mkTopuse:loadandvalue, orLOADandCOUNT? - Can
valueobserve the new count in the same cycle thatloadwrites it? - What decimal value is printed?
- Which Ready port disappears from
mkNamedCounter.v, and which remains?
Check
value carries (* result = COUNT, always_ready *) and load carries (* enable = LOAD *). Which Ready output is missing from mkNamedCounter.v?
package PortProtocol;
interface NamedCounter;
// Rename generated ports without changing the BSV method.
(* result = "COUNT", always_ready *)
method Bit#(8) value;
(* enable = "LOAD" *)
method Action load(Bit#(8) next);
endinterface
(* synthesize *)
module mkNamedCounter(NamedCounter);
Reg#(Bit#(8)) count <- mkReg(0);
method Bit#(8) value = count;
method Action load(Bit#(8) next);
count <= next;
endmethod
endmodule
(* synthesize *)
module mkTop(Empty);
NamedCounter counter <- mkNamedCounter;
Reg#(Bit#(2)) step <- mkReg(0);
rule loadValue (step == 0);
counter.load(8'h3c);
step <= 1;
endrule
rule showValue (step == 1);
$display("named counter = %0d", counter.value);
$finish(0);
endrule
endmodule
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Observation | What it proves |
|---|---|
named counter = 60 | 8'h3c is 60; the write committed one cycle before the read |
mkNamedCounter.v has outputs COUNT and RDY_load and inputs LOAD and load_next | the renamed ports exist, and load kept its default Ready and argument names |
mkNamedCounter.v has no Ready output for value | always_ready removed exactly that one port |
The printed value proves only the behavior. The port contract lives in the
generated Verilog. BSV code still calls counter.load and counter.value.
Try it yourself.
- Give
valuea state-dependent guard,if (count != 0), and keepalways_ready. GeneratingmkNamedCounternow fails with errorG0006, "Method(s) not always ready: value". A Ready port can be removed only when the method really is ready every cycle. Restore the honest handshake, or change the method so the claim becomes true.
4. Scheduling attributes
4.1 How one cycle is scheduled
To reason about a scheduling attribute, walk through one cycle in this order.
- Evaluate each rule's explicit condition from the state at the start of the cycle.
- Add the readiness of every method the rule calls; these are its implicit conditions.
- Find conflicts, such as two rules writing the same register.
- Use urgency to choose a set of enabled rules that do not conflict.
- Order the chosen rules legally and commit all their updates together at the end of the cycle.
Scheduling attributes act on steps 3 to 5. None of them can make a false condition true.
4.2 descending_urgency
When two enabled rules conflict, only one can fire. descending_urgency says
which one the scheduler considers first.
Syntax
(* descending_urgency = "moreUrgent, lessUrgent" *)(* descending_urgency = "flush, accept" *)
rule flush (flushRequested);
dropped <= count;
count <= 0;
endrule
rule accept (inputValid);
count <= count + 1;
endruleBoth rules read count and write it, so neither order works and they
conflict. When both conditions are true in the same cycle, flush fires and
accept waits. When only accept is enabled, it fires as usual. Urgency is
transitive, and a cycle such as a > b, b > c, c > a is a compile error.
Check
count starts at 5 and dropped at 0. In cycle 0 only inputValid is true; in cycle 1 both flushRequested and inputValid are true. What are count and dropped after cycle 1?
Urgency only matters for rules that conflict. Delete dropped <= count; and
the pair stops conflicting: accept can be ordered before flush, because it
reads count before flush writes it.
Check
Without dropped <= count;, flush only writes count. In cycle 1 both rules are enabled. What happens, and what does the attribute change?
4.3 execution_order and preempts
Syntax
(* execution_order = "first, second" *)
(* preempts = "winner, loser" *)(* preempts = "urgent, background" *)
rule urgent (step == 0);
a <= 1;
step <= 1;
endrule
rule background (step < 2);
b <= b + 1;
endrule| Attribute | Effect |
|---|---|
execution_order | orders rules that fire in the same cycle; if the order is incompatible with their method calls, the rules are treated as conflicting |
preempts | forces a conflict and gives the first rule priority, so the two never fire in the same cycle |
In the snippet, urgent and background write different registers and could
fire together. With preempts, background is suppressed in every cycle that
urgent fires, so after cycle 0, b is still 0. Without the attribute, both
fire in cycle 0 and b becomes 1.
Check
With (* preempts = urgent, background *) and step starting at 0, what is b after cycle 1, and what would it be without the attribute?
4.4 Rule assertions: fire_when_enabled and no_implicit_conditions
These two attributes make claims about a rule that the compiler must prove.
Syntax
(* fire_when_enabled *)
(* no_implicit_conditions *)(* fire_when_enabled, no_implicit_conditions *)
rule tick;
count <= count + 1;
endrule| Attribute | Claim |
|---|---|
fire_when_enabled | whenever the rule's explicit and implicit conditions are true, no other rule's conflict can stop it |
no_implicit_conditions | every method the rule calls is always ready |
tick satisfies both, because it only reads and writes a register. Add a call
to queue.deq and no_implicit_conditions fails with G0005. Put
fire_when_enabled on a rule that a more urgent conflicting rule can block,
and it also fails with G0005, naming the rule that blocks it.
(* descending_urgency = "inc, clear" *)
rule inc;
count <= count + 1;
endrule
(* fire_when_enabled *)
rule clear (count > 3);
count <= 0;
endruleCheck
What does bsc do with the inc and clear rules above?
4.5 Trusted assertions: mutually_exclusive and conflict_free
Syntax
(* mutually_exclusive = "r1, r2" *)
(* conflict_free = "r1, r2" *)(* mutually_exclusive = "left, right" *)
rule left (step == 1 && sel);
a <= a + 1;
endrule
rule right (step == 1 && !sel);
a <= a - 1;
endrule| Attribute | Claim the scheduler trusts |
|---|---|
mutually_exclusive | the listed rules' conditions are never true in the same cycle |
conflict_free | when the listed rules fire together, their method calls never actually conflict |
The compiler does not prove these claims. It uses them to build a less
conservative schedule, and only simulation checks for a violation. Here sel
and !sel really are exclusive, so the claim is honest.
Check
Change right's condition to step == 1 && sel, so both rules are enabled while step == 1 and sel is True. What happens at compile time and in simulation?
4.6 Example: urgency between two conflicting rules
RulePriority.bsv has two rules that both write selected and pending.
Read the initial values of pending and safetyRequested, then list every
register each selection rule writes.
In the first cycle, both conditions are true and the rules conflict.
descending_urgency makes the scheduler consider chooseSafety first; it
commits selected == 8'ha5 and clears pending. chooseNormal cannot join
that cycle. In the next cycle, both selection rules are disabled and
showSelection prints.
Predict before you run.
- Are both selection rules enabled in the first cycle?
- Which value of
selectedcommits? - Does
chooseNormalever fire?
Check
pending and safetyRequested both start True, and the attribute reads descending_urgency = chooseSafety, chooseNormal. Which value of selected does showSelection print?
package RulePriority;
(* synthesize *)
module mkTop(Empty);
Reg#(Bit#(8)) selected <- mkReg(0);
Reg#(Bool) pending <- mkReg(True);
Reg#(Bool) safetyRequested <- mkReg(True);
// Resolve the write conflict in favor of the safety rule.
(* descending_urgency = "chooseSafety, chooseNormal" *)
rule chooseSafety (pending && safetyRequested);
selected <= 8'ha5;
pending <= False;
endrule
rule chooseNormal (pending);
selected <= 8'h11;
pending <= False;
endrule
rule showSelection (!pending);
$display("selected = %h", selected);
$finish(0);
endrule
endmodule
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Observation | What it proves |
|---|---|
selected = a5 | the more urgent rule won the conflicting cycle |
| no second selection | after pending became False, neither selection rule was enabled |
Try it yourself. Make each change on its own.
- Start
safetyRequestedatFalse.chooseSafetyis disabled, urgency has nothing to decide, andchooseNormalcommits8'h11. Urgency resolves overlap; it never replaces a condition. - Reverse the attribute to
"chooseNormal, chooseSafety". The output becomes11, andbscadds warningG0021:chooseSafetycan never fire. Whenever it is enabled,pendingis true, so the more urgentchooseNormalis enabled too. - Delete the attribute. The output stays
a5, butbscreports warningG0010: it chosechooseSafetyas more urgent on its own and lists thependingread/write conflict, the same pair of warnings that Rules that share state explained. Replacingdescending_urgencywithexecution_orderproduces the same warning, because ordering two rules that cannot fire together does not resolve their conflict.
5. split and nosplit
A rule that contains an if or case normally fires as one unit, even when
the branches touch different state. (* split *) before the conditional asks
the compiler to treat each branch as its own rule fragment with its own
condition and schedule. (* nosplit *) stops splitting below that point.
Syntax
(* split *)
if (condition) action1; else action2;rule route;
(* split *)
if (sel) a <= x;
else b <= x;
endruleAfter splitting, the branch that writes a can fire alongside a rule that
only conflicts with writes to b.
Note. Independent conditionals can multiply the number of fragments
exponentially, slow scheduling, and add logic that evaluates every branch
condition each cycle. Use split only after you find a branch that is blocked
for no real reason.
6. Clock, reset, argument, and doc attributes
These attributes shape the outside of a synthesized module for integration with existing RTL.
(* synthesize, clock_prefix = "CORE_CLK", reset_prefix = "CORE_RST_N" *)
module mkCore(CoreIfc);
...
endmodule
(* doc = "routes one value per cycle" *)
rule route;
...
endrule| Target | Attributes | When to use |
|---|---|---|
| clock and reset ports | clock_prefix, gate_prefix, reset_prefix, default port attributes | when a wrapper or sign-off flow expects specific names |
| clock relationships | clock-family attributes | to declare a real relationship used to check cross-clock connections |
| module arguments | clocked_by, reset_by, port, parameter | to set an argument's domain and its port or parameter name |
| generated comments | doc | to record intent in generated RTL; it enforces nothing |
Note. Port names are part of an external contract. With
clock_prefix = "CORE_CLK", the clock input of mkCore is called CORE_CLK,
and an existing testbench that connects a port called CLK no longer
elaborates. Keep default names inside a closed BSV design.
7. Common mistakes
7.1 always_ready on a guarded method
// Wrong: the interface claims always_ready ...
interface Counter;
(* always_ready *)
method Bit#(8) value;
endinterface
// ... but the module guards the method: G0006 "Method(s) not always ready"
method Bit#(8) value if (count != 0) = count;
// Right: drop always_ready from the interface and keep the Ready port
interface Counter;
method Bit#(8) value;
endinterfacealways_ready removes Ready, so a caller could read the method when it is not
valid. The compiler refuses unless the method is ready in every state. Method
attributes belong in the interface declaration; writing one on the method
definition inside the module is error P0022, "Attributes not allowed for
method".
7.2 no_implicit_conditions on a rule that uses a FIFO
// Wrong: q.first and q.deq are not always ready, so G0005
(* no_implicit_conditions *)
rule drain;
r <= q.first;
q.deq;
endrule
// Right: drop the assertion; the rule waits for data as intended
rule drain;
r <= q.first;
q.deq;
endruleThe attribute does not remove the FIFO's implicit conditions. It claims there are none, and that claim is false.
7.3 Urgency in the wrong order
// Wrong: the general rule outranks the special one; warning G0021
(* descending_urgency = "chooseNormal, chooseSafety" *)
// Right: the narrower rule must be more urgent
(* descending_urgency = "chooseSafety, chooseNormal" *)When one rule's condition implies another's, the narrower rule has to be more
urgent, or it can never win. G0021 is only a warning, so read warnings after
every schedule change.
7.4 fire_when_enabled on a rule that can lose a conflict
// Wrong: chooseSafety is more urgent and blocks chooseNormal, so G0005
(* descending_urgency = "chooseSafety, chooseNormal" *)
rule chooseSafety (pending); ... endrule
(* fire_when_enabled *)
rule chooseNormal (pending); ... endrule
// Right: put the assertion on the rule that wins, or remove the conflictfire_when_enabled promises the rule is never blocked by another rule. A rule
that loses an urgency decision is blocked by definition.
7.5 Adding a trusted assertion to silence a warning
// Wrong: both conditions can be true at once; the claim is false
(* mutually_exclusive = "left, right" *)
rule left (step == 1); a <= a + 1; endrule
rule right (step == 1 && sel); a <= a - 1; endrulemutually_exclusive and conflict_free are believed, not proved. Write the
invariant down separately, find the state transitions that keep it true, and
only then add the claim. If you cannot make that argument, the conservative
schedule is the correct one.
8. Summary
| Topic | Key point |
|---|---|
| Syntax | (* name, name = value *) applies to the construct that follows |
synthesize, noinline | generate a separate RTL module; behavior stays, scheduling freedom may shrink |
| Port naming | result, ready, enable, port, prefix rename RTL ports only |
always_ready, always_enabled | remove a control port; the compiler must prove the claim (G0006 when false) |
| Urgency and order | descending_urgency picks a winner among conflicting enabled rules; preempts forces a conflict |
| Rule assertions | fire_when_enabled and no_implicit_conditions are proved; false claims fail with G0005 |
| Trusted assertions | mutually_exclusive and conflict_free are believed and only checked in simulation |
split | splits a conditional into separately scheduled fragments, at a compile and logic cost |
9. Check your understanding
-
Why can a separate synthesis boundary reduce concurrency without changing a method's result?
Show answer
Each method argument of a separate module is one port that carries one value per cycle, so two callers needing different arguments in one cycle conflict. An inlined module could give each caller its own logic. The computation inside the method is the same either way.
-
Which side of a method call drives Ready, and which side drives Enable?
Show answer
The module that implements the method drives Ready; it reports whether the method can serve a call. The caller drives Enable; it reports that an
Actionmethod is being called this cycle. -
Why does
descending_urgencychange the result ofRulePriority.bsvonly when both selection rules are enabled?Show answer
Urgency decides only between enabled rules that conflict. If
chooseSafety's condition is false, there is nothing to decide, andchooseNormalfires on its own. -
What is the difference between
no_implicit_conditionsandmutually_exclusivewhen the claim is false?Show answer
no_implicit_conditionsis proved by the compiler, so a false claim stops compilation withG0005.mutually_exclusiveis trusted; a false claim compiles, and only a simulation run that actually makes both conditions true can expose it. -
With
(* preempts = "urgent, background" *), the two rules write different registers. Can they fire in the same cycle?Show answer
No.
preemptsforces a conflict even when none exists, and gives the first rule priority. In any cycle whereurgentfires,backgrounddoes not.
The Practice problems below apply these contracts: a bounded buffer whose enqueue method carries a readiness claim that its own state contradicts, an unconditional tick method declared as part of an always-enabled RTL protocol, and a rule whose scheduling assertion is contradicted by the queue it depends on.