Reference

A compact look-up of every cocoa primitive, followed by the model for how a coroutine actually becomes a state machine. The cheat sheet shows what each construct looks like in source and what it compiles into; the semantics section explains the rules that make those translations work.

Cheat sheet

Process structure

1initial begin
2 // body
3end

One-shot coroutine

Runs once top-to-bottom. Must explicitly idle at the end, with forever @(posedge clk) or similar.

generates n states + a default 'done' state
1initial forever begin
2 // body
3end

Cyclic coroutine

Loops back to the top after the last @. Use for repeating protocols and continuous signals.

generates n states with a wrap-around transition
1forever @(posedge clk);

Idle tail

Suspend forever. Common at the end of an initial begin to hold outputs, or to idle after an initialization FSM.

generates one self-looping state

Time and waiting

1@(posedge clk);

One-cycle wait

Advance one clock. Every @ becomes a transition between two states.

generates transition from the current state
1@(posedge clk iff cond);

Guarded wait

Hold the current state until cond is high on a clock edge. The primary way to wait on inputs.

generates self-loop until cond; advance on the next edge
1repeat (N) @(posedge clk);

N-cycle delay

Wait exactly N clocks before continuing. N can be a parameter.

generates one self-looping state + a $clog2(N)-bit counter

Control flow

1if (cond) q = A;
2else q = B;
3@(posedge clk);

Per-state branch

Selects outputs within the current state. Both branches drive the same signals; neither contains its own @.

generates ternary mux on q inside one state
1if (cond) begin
2 q = A;
3 @(posedge clk);
4end else begin
5 q = B;
6 @(posedge clk);
7 q = C;
8 @(posedge clk);
9end

Diverging branch

When one or both branches contain @, control flow splits into separate sub-state chains and rejoins after the if/else. Each side can have its own length and outputs.

generates branch-on-cond transition + sub-states per side
1for (i = 0; i < N; i = i + 1) begin
2 acc = acc + a[i] * b[i];
3 @(posedge clk);
4end

Bounded loop

Iterate N times. If i is a module-level local the source name is preserved on the counter and you can read it as an output or use it to index arrays; otherwise an auto-counter is generated.

generates body states + counter step + loop-back
1while (!fifo_empty) begin
2 rd_en = 1'b1;
3 sum = sum + fifo_data;
4 @(posedge clk);
5end

Conditional loop

Run the body each cycle while cond holds, exit when it clears. No iteration counter is generated; bound it explicitly if needed.

generates body states + conditional exit transition

Reuse

1task automatic name(
2 input logic [7:0] a,
3 input logic b
4);
5 // body — may wait
6endtask

Task definition

A reusable sequence with input formals only. Tasks may contain @s. The call graph must be acyclic; tasks may call other tasks but not themselves.

generates inlined at each call site
1name(8'h10, 1'b1);

Task call

Inlined with arguments substituted in. Repeated sequential calls of the same task collapse into a single state controlled by a program-counter mux.

generates states from the task body, per call site

State and locals

1logic [7:0] x;
2// ... written here
3@(posedge clk);
4// ... read here

Cross-edge local

A module-level local written in one state and read in another is inferred as a flop in the generated module. No manual always_ff needed.

generates logic [N-1:0] x_q + a small always_ff
1logic [7:0] tmp;
2// used + read within one state

Combinational local

A local only touched between two @s stays as a wire — a temporary in the state's output expression.

generates intermediate wire in always_comb
1out = expr;

Output drive

Drives out combinationally for the current state, and holds into subsequent states until reassigned. No latch is inferred.

generates case row in always_comb

Semantics

States and clock waits

The coroutine body is split at every @. Each chunk between two waits becomes the output expression for one FSM state; the wait itself becomes the transition out of that state. States are numbered in source order — S0 is the first chunk, S1 the second, and so on.

1initial forever begin
2 a = 1'b1; // S0 outputs
3 @(posedge clk); // S0 -> S1
4 a = 1'b0; // S1 outputs
5 @(posedge clk); // S1 -> S0
6end

For an initial forever body the last transition wraps back to S0. For a plain initial begin the last state self-loops, holding its outputs.

Outputs and held values

Outputs are driven combinationally per state by a single always_comb block with a case on the state register. Critically, an output is held across states: assigning a = 1 in S0 and not touching a in S1 means the generated always_comb drives a = 1 in both. There is no latch — the held value is re-driven combinationally.

Where the transpiler can't prove an output is set on every path, it defaults the output to its zero value at the top of the always_comb before the case.

Locals: wires vs. flops

A module-level local is one of two things:

  • A combinational temporary if every read and write happens within the same state — between two adjacent @s. Lowered to a wire inside always_comb.
  • A flop if any read crosses an @ after a write. Lowered to logic ... x_q with a small always_ff block that captures the writes.

Counter variables in for loops follow the same rule. Because they're written in one iteration and read in the next, they're always flopped — and the source name is preserved when you declare the counter at module level.

Guarded waits

A guarded wait @(posedge clk iff cond) only fires when cond is high on a clock edge. The state holds its outputs and self-loops while the guard is false, then advances on the next edge where it's true. This is the primary way coroutines wait on inputs — req, ready, valid_in, and so on.

Branches

A condition can shape the FSM in three different ways, depending on where you put it:

  • Per-state output mux. An if/else with no @ inside. The state's output expression becomes a ternary on the condition; no new state is added. Use this when both branches drive the same signals for the same duration.
  • Diverging sub-states. An if/else where one or both branches contain @. Control flow splits at the branch and rejoins after the if/else. Each side becomes its own chain of states; the parent state gets a conditional transition out, and a join state is implicit at the rejoin point. Use this when the branches have different lengths or drive different signals.
  • Guarded wait. A condition on the wait itself: @(posedge clk iff cond). Affects only when the state advances, not what it drives.

The three forms answer different questions — what does this state drive?, which states come next?, and when does this state advance? Pick whichever matches your intent.

Loops

Loops lower to compact state-machine shapes:

  • repeat (N) @(posedge clk) is one state with an auto-counter cyc_q. The state self-loops while cyc_q < N - 1 and exits on the bound. The counter width is $clog2(N); N may be a parameter.
  • while (cond) begin … @ … end lowers to body states with a conditional exit transition: while cond holds the body runs each cycle, and when it clears the loop falls through. No iteration counter is generated; to deadline-bound it, AND in a counter check explicitly. The body may be empty (a bare @(posedge clk)), in which case it's a busy wait.
  • for (i = A; i < B; i = i + 1) begin … @ … end lowers to body states + a counter step + a loop-back transition. The body must contain at least one @ on every path so the loop makes forward progress; non-progressing loops are rejected at compile time.

Tasks and inlining

A task automatic is inlined at every call site. Each call produces its own block of states using the task body, with arguments substituted in as the values driven into the task's formals. Inlining means N call sites cost N copies of the task's states; tasks themselves don't add a state-machine layer.

When the same task is called many times in sequence, the transpiler can collapse the call sites into a single state controlled by a small program-counter register. The state's output expressions become case muxes over the PC. This is what makes the UART transmitter's ten send_bit calls compile to just two states.