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 helper reads like ordinary program logic. Control flow inside a process may describe behavior over time, so timing intent should stay visible in the source.
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:
Some expressions also mark a boundary. The sync marker is used when a public component access crosses to another context; see Contexts and Clock Domains.
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.
Chained Conditions with elif
Use elif for chained branches when more than two outcomes are possible:
public fn Classify(value: byte) int
{
if (value < 0x20)
{
return 0 // control character
}
elif (value < 0x7F)
{
return 1 // printable ASCII
}
else
{
return 2 // extended
}
}
Do not write else if. Livt uses elif for chained branches; else if is a syntax error. Each elif branch is checked only when all earlier branches were false.
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.
foreach Loops
A foreach loop iterates over a collection without a manual index variable.
Over an array — you iterate over the elements without managing an index yourself:
foreach (var b in payload)
{
this.Process(b)
}
This is equivalent to a for loop with indexing. Use whichever form makes the iteration intent clearer.
Over a cursor — the cursor component implements an iterator interface (IByteIterator, IIntIterator, etc.). The loop initializes the cursor, checks HasNext(), and reads each element with Next():
interface IByteIterator
{
fn Init()
fn HasNext() bool
fn Next() byte
}
this.cursor.Load(data, length)
foreach (var b in this.cursor)
{
sum = sum + (b as int)
}
The cursor must be a separate component field from the data it walks. Two foreach loops that share the same cursor field would corrupt each other's position.
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.
A process does not return a value. Use continue to yield back to the beginning of the process and use fields or output parameters to expose observable state.
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 hardware 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 sequencing visible when sequencing 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.