Skip to content
code0.HDL

Guiding the compiler with attributes

Attributes as requests, proved claims and trusted claims, synthesize and noinline boundaries, port naming with always_ready and always_enabled, the scheduling attributes descending_urgency, execution_order, preempts and the rule assertions, and split, clock, reset and doc attributes.

In this chapter

  • Sort an attribute into a generation request, a scheduling choice, a proved assertion or a trusted assertion, and say what happens when its claim is false
  • Predict which generated files and ports synthesize, result, enable and always_ready produce, and the G0006 error of a dishonest always_ready
  • Walk one cycle through conditions, conflicts and urgency to predict which of two conflicting rules fires under descending_urgency or preempts
  • Explain why mutually_exclusive and conflict_free must be argued from an invariant rather than from a passing simulation
Concepts in this chapter
  • Guiding the compiler with attributes
Prerequisite concepts

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
PlacementGovernsExamples
before modulethe whole modulesynthesize, clock_prefix
before rulethat rulefire_when_enabled, descending_urgency
before a method in an interfacethat method's portsresult, always_ready
before if or case inside a rulethat conditionalsplit, nosplit
before a top-level functionthat functionnoinline

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.

KindExamplesPhaseWhat happens
Generation requestsynthesize, noinline, port, clock, and reset nameselaboration and code generationsets RTL boundaries and names
Scheduling choicedescending_urgency, execution_order, preemptsschedulinglimits which enabled rules are chosen, and in what order
Proved assertionalways_ready, always_enabled, fire_when_enabled, no_implicit_conditionsstatic checkingthe compiler must prove the fact; if it cannot, compilation fails
Trusted assertionmutually_exclusive, conflict_freescheduling and simulationthe 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;
endmodule

The 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;
endfunction

Compiling 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 startRule that firesState committed at cycle end
step == 0, valueReg == 0loadValue calls load(8'h2a)valueReg == 8'h2b, step == 1
step == 1, valueReg == 8'h2bshowResult reads resultvalue printed, simulation ends

Predict before you run.

  1. How many Verilog files does the compile step generate, and what are they called?
  2. What decimal value does showResult print after load(8'h2a)?
  3. Which construct creates the one-cycle gap between loading and reading: synthesize or step?

Check

Both mkIncrementer and mkTop carry (* synthesize *), and the command generates both. How many Verilog modules does the compile step write?

Your answer

ModuleBoundary.bsv
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

endpackage

Check the result.

ObservationWhat it proves
incremented = 438'h2a is 42; the method added one, so the behavior is correct
mkIncrementer.v and mkTop.v createdthe 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.

  1. Remove the child's synthesize attribute and drop -g mkIncrementer from the command so that only mkTop is generated. The simulation still prints 43, but only mkTop.v is 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.

SignalDriven byMeaning
Ready (RDY_name)the module that implements the methodit can serve a call this cycle; this is the method's implicit condition
Enable (EN_name)the calleran 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
AttributeNames
resultthe result port of a value method
readythe Ready port
enablethe Enable port
portone method argument port
prefixthe 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 port
interface Tick;
  (* always_enabled *)
  method Action tick(Bit#(1) pulse);
  (* always_ready *)
  method UInt#(8) count;
endinterface
AttributePort removedClaim that must hold
always_readyReadythe method is ready in every cycle
always_enabledEnablethe 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.

  1. Which identifiers does mkTop use: load and value, or LOAD and COUNT?
  2. Can value observe the new count in the same cycle that load writes it?
  3. What decimal value is printed?
  4. 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?

Your answer

PortProtocol.bsv
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

endpackage

Practice this concept

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

Check the result.

ObservationWhat it proves
named counter = 608'h3c is 60; the write committed one cycle before the read
mkNamedCounter.v has outputs COUNT and RDY_load and inputs LOAD and load_nextthe renamed ports exist, and load kept its default Ready and argument names
mkNamedCounter.v has no Ready output for valuealways_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.

  1. Give value a state-dependent guard, if (count != 0), and keep always_ready. Generating mkNamedCounter now fails with error G0006, "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.

  1. Evaluate each rule's explicit condition from the state at the start of the cycle.
  2. Add the readiness of every method the rule calls; these are its implicit conditions.
  3. Find conflicts, such as two rules writing the same register.
  4. Use urgency to choose a set of enabled rules that do not conflict.
  5. 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;
endrule

Both 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?

Your answer

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?

Your answer

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
AttributeEffect
execution_orderorders rules that fire in the same cycle; if the order is incompatible with their method calls, the rules are treated as conflicting
preemptsforces 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?

Your answer

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
AttributeClaim
fire_when_enabledwhenever the rule's explicit and implicit conditions are true, no other rule's conflict can stop it
no_implicit_conditionsevery 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;
endrule

Check

What does bsc do with the inc and clear rules above?

Your answer

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
AttributeClaim the scheduler trusts
mutually_exclusivethe listed rules' conditions are never true in the same cycle
conflict_freewhen 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?

Your answer

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.

  1. Are both selection rules enabled in the first cycle?
  2. Which value of selected commits?
  3. Does chooseNormal ever fire?

Check

pending and safetyRequested both start True, and the attribute reads descending_urgency = chooseSafety, chooseNormal. Which value of selected does showSelection print?

Your answer

RulePriority.bsv
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

endpackage

Practice this concept

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

Check the result.

ObservationWhat it proves
selected = a5the more urgent rule won the conflicting cycle
no second selectionafter pending became False, neither selection rule was enabled

Try it yourself. Make each change on its own.

  1. Start safetyRequested at False. chooseSafety is disabled, urgency has nothing to decide, and chooseNormal commits 8'h11. Urgency resolves overlap; it never replaces a condition.
  2. Reverse the attribute to "chooseNormal, chooseSafety". The output becomes 11, and bsc adds warning G0021: chooseSafety can never fire. Whenever it is enabled, pending is true, so the more urgent chooseNormal is enabled too.
  3. Delete the attribute. The output stays a5, but bsc reports warning G0010: it chose chooseSafety as more urgent on its own and lists the pending read/write conflict, the same pair of warnings that Rules that share state explained. Replacing descending_urgency with execution_order produces 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;
endrule

After 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
TargetAttributesWhen to use
clock and reset portsclock_prefix, gate_prefix, reset_prefix, default port attributeswhen a wrapper or sign-off flow expects specific names
clock relationshipsclock-family attributesto declare a real relationship used to check cross-clock connections
module argumentsclocked_by, reset_by, port, parameterto set an argument's domain and its port or parameter name
generated commentsdocto 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;
endinterface

always_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;
endrule

The 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 conflict

fire_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; endrule

mutually_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

TopicKey point
Syntax(* name, name = value *) applies to the construct that follows
synthesize, noinlinegenerate a separate RTL module; behavior stays, scheduling freedom may shrink
Port namingresult, ready, enable, port, prefix rename RTL ports only
always_ready, always_enabledremove a control port; the compiler must prove the claim (G0006 when false)
Urgency and orderdescending_urgency picks a winner among conflicting enabled rules; preempts forces a conflict
Rule assertionsfire_when_enabled and no_implicit_conditions are proved; false claims fail with G0005
Trusted assertionsmutually_exclusive and conflict_free are believed and only checked in simulation
splitsplits a conditional into separately scheduled fragments, at a compile and logic cost

9. Check your understanding

  1. 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.

  2. 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 Action method is being called this cycle.

  3. Why does descending_urgency change the result of RulePriority.bsv only 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, and chooseNormal fires on its own.

  4. What is the difference between no_implicit_conditions and mutually_exclusive when the claim is false?

    Show answer

    no_implicit_conditions is proved by the compiler, so a false claim stops compilation with G0005. mutually_exclusive is trusted; a false claim compiles, and only a simulation run that actually makes both conditions true can expose it.

  5. With (* preempts = "urgent, background" *), the two rules write different registers. Can they fire in the same cycle?

    Show answer

    No. preempts forces a conflict even when none exists, and gives the first rule priority. In any cycle where urgent fires, background does 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.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows