Control flow turns values into behavior. In Livt, the familiar constructs from software languages still exist: variables, if, else, loops, return, break, and continue. Livt also adds hardware-oriented sequencing with state blocks and local goto.
The important difference is context. Control flow inside a pure function reads like ordinary program logic. Control flow inside a process may describe behavior over time and may become states in generated VHDL.
Expressions and Statements
An expression evaluates to a value:
42
value + 1
this.IsAsciiDigit(byteValue)
count == 0
A statement does something:
count = count + 1
return count
Simulation.Report("done")
Most Livt code is made from expressions inside statements. For example:
if (value >= 0x30 && value <= 0x39)
{
return true
}
The condition is an expression. The if and return are statements.
Variables
Declare local variables with var:
var count: int = 0
var valid: bool = true
var marker: byte = 0xFF
When the initializer is clear, Livt can infer the type:
var count = 0
var valid = true
Use explicit types when width or signedness matters:
var marker: byte = 0xFF
var flags: logic[8] = 0b00001111
Variables are local to the function, process, or block where they are declared. Fields belong to the component; variables belong to the current piece of code.
Conditions
Conditions should be bool expressions:
if (count == 0)
{
return true
}
else
{
return false
}
Comparisons are the usual way to turn values into conditions:
if (this.valid == 0b1)
{
this.Accept()
}
That explicit comparison is useful because valid is logic, while the if condition is bool.
Guard Clauses
A guard clause handles a special case early and returns immediately:
public fn IsAsciiDigit(value: byte) bool
{
if (value < 0x30)
{
return false
}
if (value > 0x39)
{
return false
}
return true
}
Guard clauses keep the main path of a function less nested. They are especially helpful in parsers, protocol checks, and validation functions.
Assertions
Use assert to state something that must be true during a test or simulation:
assert this.queue.IsEmpty() == true
assert value == 0x42
Assertions are most common in test components. They are also useful as local checks while developing a design.
while Loops
A while loop runs while its condition is true:
var index = 0
while (index < length)
{
index++
}
Use while when the stopping condition is the clearest way to express the loop. In hardware-oriented code, make sure the loop has an obvious bound or a clear reason to terminate.
for Loops
A for loop keeps initialization, condition, and increment in one place:
var sum = 0
for (var index = 0; index < 4; index++)
{
sum = sum + values[index]
}
Use for when iterating over a fixed range, array, or known number of cycles. Fixed bounds are easy to review and map naturally to hardware and simulation.
break and continue
break exits a loop early:
for (var index = 0; index < length; index++)
{
if (payload[index] == 0x00)
{
break
}
}
continue skips the rest of the current loop iteration:
for (var index = 0; index < length; index++)
{
if (payload[index] == 0x00)
{
continue
}
this.Process(payload[index])
}
Inside a for loop, continue advances through the increment step before the next condition check. Inside a while loop, it jumps back to the condition.
Process-Level continue
Processes run repeatedly. In a process body, continue has an additional meaning: restart the process on the next cycle.
That makes it useful for polling:
process Main()
{
if (this.inputAvailable == false)
{
continue
}
this.HandleInput()
}
The process checks whether input is available. If not, it yields and tries again on the next cycle. If input is available, it continues with the work.
Use this pattern when a process should wait for a condition without doing the rest of its body.
Blocks
Curly braces group statements:
if (enabled)
{
count = count + 1
this.lastSeen = count
}
In functions, a block is mostly a lexical grouping. In processes, grouping can also help make multi-step behavior easier to read.
State Blocks
Some hardware behavior is naturally multi-step: wait for input, capture data, process it, write a result, then return to idle. Livt uses state blocks to make those steps explicit.
An anonymous state groups several statements into one step:
state {
this.valid = 0b1
this.data = 0x41
}
A named state can be used as a local jump target:
process Main()
{
state Idle
{
if (this.start == 0b1)
{
goto Load
}
goto Idle
}
state Load
{
this.value = this.input
goto Done
}
state Done
{
this.ready = 0b1
goto Idle
}
}
goto is deliberately local. It jumps only to named states in the current process. It is not a general-purpose cross-function jump.
When to Use Explicit States
Do not turn every function into a hand-written state machine. Use ordinary conditions, loops, and functions when they express the behavior clearly.
Use named states when the design has real sequencing over time:
- protocol handshakes
- multi-cycle algorithms
- waiting for external input
- staged reads or writes
- simulation tests that need to let time pass
The point of state is not to make code look low-level. The point is to make time visible when time is part of the design.
Waiting in Tests
Tests sometimes need to let a design run for a few cycles before checking a result. Empty states are one simple way to advance time:
state {}
state {}
state {}
For longer waits or clearer intent, simulation helpers may be more readable. Use the form that makes the test easiest to understand.
Summary
Control flow in Livt has two layers:
- Familiar program flow: variables,
if, loops,return,break, andcontinue. - Hardware-oriented sequencing: process-level
continue,state, and localgoto.
Use bool expressions for decisions. Compare logic signals explicitly. Keep loops bounded and easy to review. Reach for named states when behavior genuinely spans multiple steps in time.