How is procedural code converted into a circuit?
It is actually quite easy. First all loops are unrolled (the code is not synthesizable if they can't be unrolled) and all functions and tasks are inlined. After that you are only left with if- and case-statements and assignments (if- and case-statements are essentially the same). the if statements can be transformed into multiplexers with the conditions on the select inputs. For example

if (cond1) begin
   if (cond2)
      foobar &lt;= expr1;
end else
   foobar &lt;= expr2;


is the same as

foobar &lt;= cond1 ? (cond2 ? expr1 : foobar) : expr2;


(notice how the old value of foobar is used in the cond1 &amp;&amp; !cond2 case). Of course there are a couple of extra steps to handle blocking assignments, asynchronous resets, etc. But this is the basic idea.

I recommend using a synthesis tool on simple code snippets to learn more about the relationship between your code and the generated circuit. You can use of course whatever tool you are using already, but I can't write a response to your question without mentioning Yosys, the Verilog synthesis tool I wrote. The command yosys -p "proc;; show" test.v will translate the module in test.v to a RTL netlist and display the circuit (needs graphviz and xdot installed). This will, for example, transform this into this.

Update: Farhad Yusufali asked in the comments about blocking assignments.

Consider the following code:

always @(posedge clk) begin
  a = x;
  if (b) begin
    c &lt;= c + a;
    a = y;
  end
  d &lt;= a;
  a = a + 1;
  e &lt;= a;
end


now we can write this as two always blocks using only nonblocking assignment like this:

always @* begin
  a_1 &lt;= x;
  if (b)
    a_2 &lt;= y;
  else
    a_2 &lt;= a_1;
  a_3 &lt;= a_2 + 1;
end

always @(posedge clk) begin
  if (b)
    c &lt;= c + a_1;
  d &lt;= a_2;
  e &lt;= a_3;
end


I.e. whenever a is used on the left side of an assignment, we move the assignment to the asynchronous block and change a to a_&lt;index&gt;. Whenever a is used on the right hand side we replace it with the last a_&lt;index&gt; we created. When we branch (such as in if (b)) and create a new a_&lt;index&gt; in one of the branches, then you have to make sure that the last assignment to a version of a in each branch goes to the same index, adding extra assignments as needed (such as the assignment of a_2 in the else branch).

This notation of replacing a variable with multiple variables with indices in this way is a variation of what is known as SSA form in the compiler design community.