Language reference

Building Blocks

Namespaces, components, fields, constants, constructors, processes, functions, and interfaces — the constructs from which all Livt designs are made.

Every programming language has its smallest set of constructs — the building blocks from which larger designs are made. In Livt, these constructs are designed to balance clarity, reusability, and FPGA-specific precision. By understanding them, you gain the vocabulary to describe both small circuits and large systems.

This chapter walks through these building blocks step by step: namespaces, components, fields, constants, constructors, processes, functions, and interfaces. Small examples throughout demonstrate how these constructs fit together in real designs.

Namespaces

Namespaces are the outermost construct in Livt. They provide a way to group related components, functions, and interfaces into logical units. This helps prevent naming conflicts and keeps larger projects organized.

Namespaces in Livt can be file-scoped or block-scoped. For most cases — and especially in this book — we use file-scoped namespaces, which are simple and keep nesting shallow.

livt
namespace Example.App

Everything declared in this file belongs to the Example.App namespace. If no namespace is specified, the global namespace is used. While this is discouraged in real projects, many examples in this book omit namespaces to keep the code focused on the concepts being taught.

Components

A component is the fundamental unit of design in Livt. Components represent hardware modules: they can have inputs, outputs, fields, constants, processes, and functions. Components are where your FPGA logic lives.

livt
component AndGate {}

Here, AndGate is an empty component — a placeholder for what we will build next. We start with simple components to grasp Livt's concepts, but the same principles apply to complex components as well.

Fields

Fields represent the internal state of a component. They can store values or hold intermediate signals. Fields without an access modifier are private by default. The logic data type should be familiar if you've worked with VHDL. Private fields are only accessible from within the component itself.

livt
component AndGate
{
    a: logic
    b: logic
    c: logic
}

Fields can also be made public, making them accessible outside the component. For public fields, the direction must be specified for output fields. For inputs, the direction can be omitted since in is the default.

livt
component AndGate
{
    public a: logic
    public b: logic
    public c: out logic
}

Constants

Constants are immutable values defined inside a component or interface. They provide read-only data that never changes during simulation or synthesis. Typical uses include configuration parameters, fixed addresses, default values, and widths that determine the size of signals and buses.

livt
component Counter
{
    const WIDTH: int = 8
    value: logic[WIDTH]
}

By default, constants are private to the component. Just like fields, they can also be declared public to make them accessible outside the component:

livt
component Counter
{
    public const WIDTH: int = 8
    value: logic[WIDTH]
}

Public constants are especially useful when multiple components share the same fixed value — for example, a clock frequency, buffer size, or protocol-related identifier. Instead of duplicating numbers across the design, you define them once and reuse them consistently.

Cross-Component Example

The following example shows how a public constant can be defined in one component and reused in another:

livt
component ClockConfig
{
    public const CLOCK_FREQ: int = 50_000_000  // 50 MHz
}

component Uart
{
    baud_rate: int

    new()
    {
        // Use the shared clock frequency from ClockConfig
        this.baud_rate = ClockConfig.CLOCK_FREQ / 115200
    }
}

If the system clock changes, only the constant in ClockConfig needs to be updated, and every dependent component will automatically use the new value.

Constructors

Constructors in Livt define how a component is wired when created. They establish connections between a component's internal fields and the external signals or subcomponents it communicates with.

By convention, constructor parameters are inputs unless explicitly marked with out. Inside the constructor body, input parameters are assigned to the component's fields, and outputs are exposed back to the caller. The constructor acts as a bridge between the outside world and the component's internal logic.

livt
component AndGate
{
    public a: logic
    public b: logic
    public c: out logic

    new(a: logic, b: logic, c: out logic)
    {
        this.a = a
        this.b = b
        c = this.c
    }

    process Compute[]()
    {
        this.c = this.a & this.b
    }
}

The constructor makes the component usable: it accepts two input signals and an output, wires them into the gate's internal fields, and exposes the result. Without it, the fields would exist but remain unconnected.

Instantiating a Component

Constructors become especially useful when a component is instantiated inside another:

livt
component SmallSystem
{
    gate1: AndGate

    new(a: logic, b: logic, c: out logic)
    {
        // Create and wire an AndGate instance
        this.gate1 = new AndGate(a, b, c)
    }
}

Composing Two Components

Constructors also make it natural to compose multiple components. Here is a three-input AND gate built from two 2-input AndGate instances:

livt
component ThreeInputAnd
{
    y1: logic

    gate1: AndGate
    gate2: AndGate

    new(a: logic, b: logic, c: logic, y: out logic)
    {
        // First AND gate combines a and b
        this.gate1 = new AndGate(a, b, this.y1)

        // Second AND gate combines gate1 output with c
        this.gate2 = new AndGate(this.y1, c, y)
    }
}

gate1 computes a & b; gate2 computes (a & b) & c, producing the final output y. This approach scales naturally — large FPGA systems are always built hierarchically from smaller components.

Top-Level Considerations

The top-level component — the one that interfaces directly with hardware — must only use primitive signals (such as logic) in its constructor. This ensures that the FPGA I/O remains simple, explicit, and synthesizable. Subcomponents can use references freely, but the top level acts as the clean boundary to the physical hardware.

Best Practices

  • Use descriptive names for constructor parameters — they define the component's interface.
  • Expose only what's necessary with out parameters. Keep implementation details hidden.
  • Keep wiring logic simple. Behavior should live in processes or functions.
  • Leverage composition by wiring smaller components into larger ones, while keeping top-level constructors flat and primitive.

Processes

Processes in Livt describe the ongoing behavior of a component. They are the counterpart to VHDL processes but with cleaner, more approachable syntax. A process in Livt runs continuously — once it reaches the end, execution starts again from the beginning. This looping execution makes processes ideal for describing hardware behavior that reacts to inputs and updates outputs over time.

The [] notation indicates a combinational process with no clock or reset context. Whenever any input changes, the output is updated immediately:

livt
component AndGate
{
    a: logic
    b: logic
    c: logic

    new(a: logic, b: logic, c: out logic)
    {
        this.a = a
        this.b = b
        c = this.c
    }

    process AndProcess[]()
    {
        this.c = this.a & this.b
    }
}

A sequential process (without []) uses the component's default context — it reacts to the component's clock and reset signals:

livt
component Counter
{
    count: logic[8]   // 8-bit counter

    new(clk: clock, rst: reset, count: out logic[8])
    {
        // Assign clock and reset explicitly
        this.context.clk = clk
        this.context.rst = rst
        count = this.count
    }

    process CountProcess()
    {
        this.count = this.count + 1
    }
}

The counter increments on every rising edge of the clock (the default behavior in Livt). The clock and reset are passed in via the constructor and bound to the component's context — there is no need to declare them as fields.

Functions

Functions in Livt are reusable blocks of logic that can accept parameters and return values. Unlike processes, which run continuously, functions execute once when called and then stop. This makes them ideal for calculating derived values, performing helper operations, or encapsulating reusable logic that doesn't need to run on every clock cycle.

livt
component MathHelper
{
    fn Abs(value: int) int
    {
        if (value < 0)
        {
            return -value
        }
        return value
    }
}

Functions can accept multiple parameters. Here is a function that checks whether a value falls within a range:

livt
component RangeChecker
{
    fn IsInRange(value: int, min: int, max: int) bool
    {
        return (value >= min) && (value <= max)
    }
}

The key difference between functions and processes becomes clear when we compare them side by side:

livt
component Comparator
{
    a: logic[8]
    b: logic[8]
    greater: logic

    // Process: runs continuously, updates output field
    process Compare[]()
    {
        this.greater = this.a > this.b
    }

    // Function: called explicitly, returns a value
    fn Max(x: logic[8], y: logic[8]) logic[8]
    {
        if (x > y)
        {
            return x
        }
        return y
    }
}

Functions may be private (internal use only) or public (part of the component's API). Hiding internal state while exposing only the necessary functions makes components simpler to use and more maintainable. The following FIFO example demonstrates both:

livt
component Fifo
{
    buffer: logic[8][16]   // 16 entries of 8 bits
    read_pos: logic[4]
    write_pos: logic[4]

    // Private function - internal use only
    fn IsFull() bool
    {
        return (this.write_pos + 1) == this.read_pos
    }

    // Public function - external interface
    public fn Push(data: logic[8]) bool
    {
        if (this.IsFull())
        {
            return false  // Could not push, FIFO is full
        }
        this.buffer[this.write_pos] = data
        this.write_pos = this.write_pos + 1
        return true  // Successfully pushed
    }

    // Public function - external interface
    public fn Pop(data: out logic[8]) bool
    {
        if (this.read_pos == this.write_pos)
        {
            return false  // Could not pop, FIFO is empty
        }
        data = this.buffer[this.read_pos]
        this.read_pos = this.read_pos + 1
        return true  // Successfully popped
    }
}

Interfaces

Interfaces in Livt define contracts without providing implementation. They specify function signatures, declare fields, and hold constants, but leave the actual behavior to the components that implement them. This makes interfaces powerful tools for building reusable, interchangeable designs.

In traditional HDL design, reusability is challenging. Interfaces solve this by establishing a standard contract. Once multiple components implement the same interface, they become interchangeable — you can swap one for another without changing the surrounding system.

A Simple Interface

livt
interface IMemory
{
    fn Read(address: logic[16]) logic[8]
    fn Write(address: logic[16], data: logic[8])
}

This interface defines a contract: any component implementing IMemory must provide Read and Write functions with these exact signatures.

A component implements an interface by declaring it with the : operator:

livt
component SimpleRam : IMemory
{
    storage: logic[8][65536]  // 64KB of memory

    public fn Read(address: logic[16]) logic[8]
    {
        return this.storage[address]
    }

    public fn Write(address: logic[16], data: logic[8])
    {
        this.storage[address] = data
    }
}

Interfaces can also hold constants to define shared protocol parameters:

livt
interface IBus
{
    const DATA_WIDTH: int = 32
    const ADDR_WIDTH: int = 16

    fn Read(address: logic[ADDR_WIDTH]) logic[DATA_WIDTH]
    fn Write(address: logic[ADDR_WIDTH], data: logic[DATA_WIDTH])
}

Interfaces can also declare fields that implementing components must provide:

livt
interface ICounter
{
    value: logic[8]

    fn Increment()
    fn Reset()
}

component UpCounter : ICounter
{
    public value: logic[8]

    public fn Increment()
    {
        this.value = this.value + 1
    }

    public fn Reset()
    {
        this.value = 0
    }
}

Interfaces bring real design benefits:

  • Reusability: Components implementing the same interface can be swapped freely.
  • Testability: Mock implementations make unit testing straightforward.
  • Modularity: Interfaces define clear boundaries between system parts.
  • Maintainability: Changes to internal logic don't affect external contracts.

Summary

This chapter introduced the fundamental building blocks of Livt, the constructs from which all FPGA designs are built:

  • Namespaces organize code into logical groups, preventing naming conflicts and keeping large projects manageable.
  • Components are the heart of Livt — hardware modules with fields, constants, constructors, processes, and functions.
  • Fields hold a component's internal state, private by default or public when accessible from outside.
  • Constants provide immutable configuration values, and when made public, enable shared parameters across components.
  • Constructors define how components are wired — connecting internal fields to external signals and enabling composition.
  • Processes describe ongoing behavior. Sequential processes use the component context; combinational processes (marked []) update outputs immediately.
  • Functions encapsulate reusable logic that executes once when called, either private for internal use or public as an external API.
  • Interfaces define contracts without implementation, making components interchangeable and designs dramatically more reusable.

In the next chapter, we will explore data types and operators — the mechanisms Livt uses to represent and manipulate values, from simple logic bits to complex structures and arrays.