Language reference

Control Flow

Expressions, statements, conditions, loops, and hardware-centric FSM sequencing — the tools for making a Livt design do something useful.

Up to this point we focused on what data looks like (types) and how to combine it (operators). The next step is making a design do something useful: execute code conditionally, repeat behavior, and sequence actions over time.

In a purely software language, control flow is mostly about the order in which CPU instructions run. In Livt, control flow has a hardware-flavored interpretation as well: many statements naturally project to states in an FSM in the generated VHDL. That does not change how you write Livt, but it explains why the language has explicit constructs for grouping statements into a single "step".

Expressions

An expression is anything that evaluates to a value. You use expressions everywhere: on the right-hand side of assignments, as arguments to function calls, and as conditions for if and loops.

The simplest expressions are literals and names:

livt
42
"Hello"
true
my_counter

From there you build composite expressions by combining smaller ones with operators and calls:

livt
var a: int = 3
var b: int = 5
var sum = a + b
var ok: bool = (a < b) && (b < 10)

As described in the previous chapter, && and || short-circuit: the right-hand side is only evaluated if it is needed to determine the result. This becomes especially important when the right-hand side calls functions that read inputs, access arrays, or interact with simulation.

When expressions get complex, prefer parentheses even if you know the precedence rules. They make intent obvious and prevent subtle mistakes:

livt
var a: int = 3
var b: int = 5
var sum = a + b
var is_small: bool = sum < 10

Statements

If expressions compute values, statements do work. They drive simulation output, modify state, and form the steps your design executes. Many everyday statements in Livt are simply expressions used for their effect:

livt
Simulation.Report("Hello World!")

Simulation.Report("Hello World!");  // trailing semicolon also valid
a + b;                               // evaluates, result is discarded

In day-to-day Livt code you will most often see:

  • Assignments (a = a + 1)
  • Calls used for their side effect (Simulation.Report(...))
  • Control-flow statements such as if, loops, and return

Blocks and FSM Steps

A statement typically maps cleanly to a single FSM state in the generated VHDL. If you need multiple statements where a single state is expected, wrap them in a state block:

livt
state {
    Simulation.Report("First statement.")
    Simulation.Report("Second statement.")
}

Guard Clauses and return

In functions, control flow is often simplest when you handle special cases early and return immediately. This style is called a guard clause, and it keeps the "main path" of the function less nested.

The following Fibonacci implementation demonstrates guard clauses and early returns:

livt
public fn GetFibonacci(n: int) int
{
    var a = 0
    var b = 1
    var c = 0

    if (n == 0)
    {
        return a
    }

    if (n == 1)
    {
        return b
    }

    for (var i = 2; i <= n; i++)
    {
        c = a + b
        a = b
        b = c
    }

    return c
}

return is a statement that exits the function immediately. Early returns are often clearer than a single large if/else nest.

Variables

You declare variables using var. If you omit an initializer, the variable's value defaults to a sensible default depending on its type: zero for primitive types, false for bool, and null for references.

livt
var my_string = "here is my value"
var my_integer = 1 + 5
var my_reference: MyComponent = null

Livt also supports type deduction on first assignment:

livt
var my_variable
my_variable = 123

Once declared, you can read and assign a variable using its name:

livt
var my_value = "Hello"
Simulation.Report(my_value)  // Hello
my_value = "World"
Simulation.Report(my_value)  // World

Conditions

Branching selects which statement (or block) to execute based on a condition. Conditions are typically bool expressions — comparisons and equality checks produce bool, which makes them the natural building blocks for if.

livt
if (condition)
{
    Simulation.Report("yes")
}
else
{
    Simulation.Report("no")
}

Conditions from Signals

In hardware-oriented code you often start from a logic signal and build a boolean condition by comparing it to a literal. For example, a "data valid" bit stored as logic becomes a bool condition like this:

livt
if (this.dv == 0b1)
{
    Simulation.Report("valid")
}

This keeps the control flow firmly in bool land while still working with multi-valued logic signals.

else if Chains

When you have more than two alternatives, you can chain conditions with else if. This is common when decoding a small state or a selector signal:

livt
if (this.sign == 0b01)
{
    this.HandlePositiveSign()
}
else if (this.sign == 0b10)
{
    this.HandleNegativeSign()
}
else
{
    // No special handling needed for other values.
}

Assertions

Control flow is usually written around assumptions ("this index is in range", "the queue isn't empty", "this state is reachable"). In Livt you can encode those assumptions directly using assert:

livt
assert this.my_queue.IsEmpty() == true;
assert value == 0b1;

Assertions are especially useful when used as guards: check a precondition first, then write the rest of the logic assuming it holds.

Loops

Loops repeat a behavior as long as a condition holds, or for a fixed number of steps.

While Loop

A while loop executes the body repeatedly as long as the condition expression evaluates to true. Use while when the stopping rule is naturally expressed as a condition and the number of iterations is not fixed ahead of time.

livt
var a = 1
while (a < 10)
{
    Simulation.Report(a)
    a = a + 1
}

For Loop

Livt also provides a classic C-style for loop. It keeps the loop counter logic in one place and is especially useful for counted repetition:

livt
for (var a = 1; a < 10; a++)
{
    Simulation.Report(a)
}

A very common use of for is accumulating a value:

livt
public fn GetSum() int
{
    var sum = 0

    for (var x = 0; x < 10; x++)
    {
        sum = sum + x
    }

    return sum
}

For-Each Loop

For iterating over sequence-like types, Livt provides a for-each loop. The collection must implement the Iterator interface. This form is ideal when you do not care about indices and simply want to process each element in sequence.

livt
for (a : my_list)
{
    Simulation.Report(a)
}

FSM-Style Control Flow

So far we treated control flow like a software concept. In Livt, control flow is also used to describe multi-step behavior that spans clock cycles. The idiomatic way to express this is a process built from state blocks.

Anonymous vs Named States

There are two forms of state blocks:

  • state { ... } is an anonymous state. It groups multiple Livt statements into a single step.
  • state MyState { ... } is a named state. It explicitly models an FSM step and can be the target of a goto.

In FPGA terms, think of a state block as a place where you decide what happens in one step. A single Livt statement often maps to one FSM state in generated VHDL, but an anonymous state { ... } block lets you group multiple statements so they map together as one hardware state.

goto

Livt provides goto for FSM-style code. This is not an unrestricted "jump anywhere" like in C. A goto target must be a named state, and the jump is constrained to the current process where the state is declared. This keeps control flow explicit and prevents cross-function or cross-process spaghetti.

States flow through in order by default — you only change that flow when you introduce an explicit jump. The following example shows the structure of an FSM-style process:

livt
process GCD[]()
{
    state Idle
    {
        if (this.dv == 0b1)
        {
            goto Load
        }
        else
        {
            goto Idle
        }
    }

    state Load
    {
        // ... drive control signals ...
        goto Wait
    }

    state Wait
    {
        if (this.sign == 0b01)
        {
            goto WriteA
        }
        else if (this.sign == 0b10)
        {
            goto WriteB
        }
        else
        {
            goto Idle
        }
    }

    state WriteA { goto Wait }
    state WriteB { goto Wait }
}

This style makes state transitions explicit and keeps each step small and easy to reason about. It also maps directly to how you would implement the same behavior as a VHDL FSM.

When Does FSM-Style Make Sense?

You do not need to force every piece of logic into a manually-written FSM. In many cases, straightforward statements and function calls are enough. FSM-style becomes a good fit when you need to describe sequencing across time, for example:

  • Handshakes and protocols with multi-step interactions
  • Algorithms that naturally proceed step-by-step over many cycles
  • Explicit control of "what happens next" depending on external inputs

When you reach that point, using named states plus local goto transitions can be clearer than deeply nested control flow.

Waiting with Empty States

When simulating, empty states are a simple way to advance time for a few steps, giving a design a chance to advance its internal state machine before you check results:

livt
state {}
state {}
state {}

Summary

This chapter introduced the core control-flow constructs you use to turn calculations into behavior:

  • Expressions evaluate to values; statements perform effects. Grouping statements into state { ... } blocks structures multi-step behavior in a way that maps naturally to hardware.
  • Guard clauses with early return keep functions flat and readable by handling special cases first.
  • Variables are declared with var and support type deduction.
  • Conditions: if / else selects between alternatives, driven by bool expressions.
  • Assertions encode preconditions directly in code using assert.
  • Loops: while repeats while a condition holds, for expresses counted repetition, and for-each iterates over Iterator-implementing types.
  • FSM-style: named state blocks and local goto transitions provide a clear, synthesizable way to describe multi-cycle behavior.

In the next chapter, we will explore how to organize your code into namespaces and components, and how to structure larger Livt projects for maintainability.