Language reference

Control Flow

Expressions, statements, conditions, loops, and hardware-oriented sequencing: the tools for making Livt designs do useful work.

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:

livt
42
value + 1
this.IsAsciiDigit(byteValue)
count == 0

A statement does something:

livt
count = count + 1
return count
Simulation.Report("done")

Most Livt code is made from expressions inside statements. For example:

livt
if (value >= 0x30 && value <= 0x39)
{
    return true
}

The condition is an expression. The if and return are statements.

Variables

Declare local variables with var:

livt
var count: int = 0
var valid: bool = true
var marker: byte = 0xFF

When the initializer is clear, Livt can infer the type:

livt
var count = 0
var valid = true

Use explicit types when width or signedness matters:

livt
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:

livt
if (count == 0)
{
    return true
}
else
{
    return false
}

Comparisons are the usual way to turn values into conditions:

livt
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:

livt
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:

livt
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:

livt
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:

livt
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:

livt
for (var index = 0; index < length; index++)
{
    if (payload[index] == 0x00)
    {
        break
    }
}

continue skips the rest of the current loop iteration:

livt
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:

livt
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:

livt
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:

livt
state {
    this.valid = 0b1
    this.data = 0x41
}

A named state can be used as a local jump target:

livt
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:

livt
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, and continue.
  • Hardware-oriented sequencing: process-level continue, state, and local goto.

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.