Language reference

Building Blocks

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

Livt programs are built from a small set of language constructs: namespaces, components, fields, constants, constructors, processes, functions, and interfaces. These constructs let you describe hardware in a structured way without dropping down to primitive gates or hand-written VHDL for every detail.

This chapter gives you the vocabulary for reading and writing Livt code. The examples are intentionally small, but they use the same concepts you will use in larger systems: explicit boundaries, named state, reusable behavior, and clear contracts between components.

Namespaces

A namespace groups related declarations and prevents names from colliding as a project grows. In most files, the namespace appears once at the top:

livt
namespace Livt.App

Everything declared in that file belongs to Livt.App. A file can then import another namespace with using:

livt
namespace Livt.App.Tests

using Livt.App

After the using, code in Livt.App.Tests can refer to components from Livt.App by their short names.

Use namespaces to reflect the structure of the project. For example, application logic, protocol helpers, tests, and reusable library code should usually live in different namespaces.

Components

A component is the main unit of Livt design. It is similar to a hardware module: it can hold state, expose public behavior, connect to other components, and generate VHDL.

An empty component is valid:

livt
component PacketStatistics
{
}

Real components usually contain fields, constants, functions, processes, or subcomponents. A component should represent a meaningful design boundary, not a single primitive operation. For example, prefer a component such as PacketStatistics, FrameParser, or RegisterBank over a component that merely wraps one boolean operator.

Fields

Fields are values owned by a component. The most important distinction is whether a field is private or public, and whether it is a stored value or a signal.

Private Stored Fields

A field without public is private. It belongs to the component and is not accessible from outside:

livt
component PacketStatistics
{
    acceptedCount: int
    droppedCount: int

    public fn Accept()
    {
        this.acceptedCount = this.acceptedCount + 1
    }

    public fn Drop()
    {
        this.droppedCount = this.droppedCount + 1
    }
}

acceptedCount and droppedCount are implementation details. Other components cannot read or write them directly. If the component wants to expose the values, it should provide a public function or a public stored field.

Public Signal Fields

A public field with a direction is part of the component boundary. Direction tells which side drives the signal:

livt
component ReadyValidMonitor
{
    public valid: in logic
    public ready: in logic
    public transfer: out logic
}

Use in for a signal driven by the caller and read by the component. Use out for a signal driven by the component and read by the caller. Public signal fields should be explicit; do not rely on an implied direction. The component body will later contain functions or processes that read the inputs and drive the outputs.

Public Stored Fields

A public field without a direction is a stored public value:

livt
component PacketStatistics
{
    public acceptedCount: int

    public fn Accept()
    {
        this.acceptedCount = this.acceptedCount + 1
    }
}

This is different from public acceptedCount: out int. A public stored field is state owned by the component. It is useful for status registers, counters, and small observable values that callers should be able to read directly.

Use public stored fields deliberately. If a value is an implementation detail, keep it private and expose behavior through functions.

Constants

Constants are named values that do not change. They are useful for widths, fixed addresses, protocol values, and limits:

livt
component UartConfig
{
    public const CLOCK_HZ: int = 50_000_000
    public const BAUD: int = 115_200
    public const DIVIDER: int = CLOCK_HZ / BAUD
}

Constants can be private or public. A public constant is part of the component's API and can be reused by other components:

livt
component UartTiming
{
    ticksPerBit: int

    new()
    {
        this.ticksPerBit = UartConfig.DIVIDER
    }
}

Constants can also hold fixed-size data:

livt
component HttpText
{
    public const CRLF: byte[2] = [0x0D, 0x0A]
    public const OK: byte[] = "OK".Encode()
}

Use constants instead of repeating magic numbers. They make intent visible and keep protocol code easier to review.

Keep constants simple: primitive values, strings, and fixed-size primitive array data are good constant material. Components and interfaces are structural references, not constants.

Use uint for constants that are counts or sizes and should never be negative:

livt
public const MAX_PACKET_SIZE: uint = 1500
public const BUFFER_DEPTH:    uint = 32

Chapter 3 explains int and uint in detail.

Constructors

A constructor describes how a component is created and wired. It is introduced with new:

livt
component StatusOutput
{
    enabled: logic
    public active: out logic

    new(enabled: logic, active: out logic)
    {
        this.enabled = enabled
        active = this.active
    }
}

The constructor receives an input signal named enabled and an output signal named active. Inside the constructor, this.enabled = enabled stores the input binding on the component, while active = this.active exposes the component's public output to the caller. The constructor establishes the connection; the component's behavior is described separately in functions or processes.

Constructor parameters are how components connect to the outside world and to each other. Keep constructor wiring simple. Behavior belongs in functions and processes; constructors should mainly bind fields, outputs, and subcomponents.

Instantiate components with new ComponentName(...). A component name followed by parentheses, such as ComponentName(), is not a function call and is not valid construction syntax.

It is common and readable for a constructor parameter to have the same name as the internal field it initializes:

livt
component Buffer
{
    depth: uint

    new(depth: uint)
    {
        this.depth = depth
    }
}

Inside functions, processes, and constructors, avoid declaring local variables that reuse a parameter name. A local variable should not hide the value passed into the callable.

Subcomponents

A component can own another component as a field and instantiate it in the constructor:

livt
component PacketPipeline
{
    stats: PacketStatistics

    new()
    {
        this.stats = new PacketStatistics()
    }

    public fn AcceptPacket()
    {
        this.stats.Accept()
    }
}

This is composition. Larger systems are built by wiring smaller components together, but each component should still have a meaningful responsibility.

A constructor can also call methods on a subcomponent after instantiating it. This is useful for one-time setup that happens before the design starts running:

livt
component RegisteredPipeline
{
    stats: PacketStatistics
    limit: int

    new(maxPackets: int)
    {
        this.stats = new PacketStatistics()
        this.stats.SetLimit(maxPackets)
    }
}

These calls are executed once on startup. The compiler generates a startup sequence that runs automatically after reset, before the design's processes begin operating.

Top-Level Boundaries

The top-level component is the boundary to the physical FPGA or ASIC. Keep that boundary explicit. Constructor parameters at the top level should be primitive signals or well-defined interface bundles that map cleanly to generated VHDL ports.

Inside the design hierarchy, components can pass subcomponents and interfaces around more freely. At the edge of the hardware, make the signal contract obvious.

Processes

Processes describe ongoing behavior. A process is different from a function: it is part of the component's hardware behavior and runs repeatedly.

Combinational Processes

A process with empty brackets has no clock or reset context:

livt
component ReadyValidMonitor
{
    public valid: in logic
    public ready: in logic
    public transfer: out logic

    process Update[]()
    {
        this.transfer = this.valid & this.ready
    }
}

process Update[]() is combinational. It describes output behavior that depends directly on current inputs.

Sequential Processes

A process without empty brackets uses the component's context, which provides clock and reset:

livt
component PacketCounter
{
    public count: int

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

This process updates count over time. In generated hardware, that means state: the value is held and updated according to the component's clock and reset.

Context

Every component has a context for sequential behavior. In many examples it is implicit. When a component receives clock and reset signals explicitly, it can bind them in the constructor:

livt
component TimedCounter
{
    public count: int

    new(clk: clock, rst: reset)
    {
        this.context.clk = clk
        this.context.rst = rst
    }

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

Use process Name[]() for combinational behavior and process Name() for clocked behavior. This visual distinction is one of the simplest ways Livt keeps timing intent visible in source code.

Processes do not have return types and are not called like functions. A process describes behavior that is part of the component and runs according to its combinational or clocked context.

Functions

Functions describe reusable behavior that runs when called. They can compute values, update component state, or provide a public API.

livt
component ByteClassifier
{
    public fn IsAsciiDigit(value: byte) bool
    {
        return value >= 0x30 && value <= 0x39
    }
}

The return type appears after the parameter list. If a function has no return type, it performs an action and returns no value.

Function Context, Static Functions, and Inline Helpers

Functions have the same context marker idea as processes. A normal function, written as fn Name(...), is context-bound. It uses the component context and is lowered as a scheduled operation with the normal start/busy/return protocol.

A function written with empty context brackets, fn Name[](...), is context-free. It is intended for callable combinational helper logic that does not need a clock or reset. With a return type it lowers to a package-level VHDL function:

livt
component ByteClassifier
{
    fn IsAsciiDigit[](value: byte) bool
    {
        return value >= 0x30 && value <= 0x39
    }

    public fn Check(value: byte) bool
    {
        return this.IsAsciiDigit(value)
    }
}

Use process Name[]() for always-present combinational component behavior. Use fn Name[](...) for callable context-free helper logic.

If a context-free helper has no return type, it should produce a visible effect. It can update an explicit out or inout parameter, or mutate a directionless array parameter:

livt
component ResultHelpers
{
    fn Fill[](value: out int)
    {
        value = 42
    }

    public fn Run() int
    {
        var result: int
        this.Fill(result)
        return result
    }
}

For example, a directionless array parameter is mutable and returns indexed updates to the caller:

livt
fn SetByte[](values: byte[], index: int, value: byte)
{
    values[index] = value
}

A no-return context-free helper with no field write, out/inout parameter write, or mutable array parameter write is legal, but Livt warns that calls to it have no observable effect.

inline is a separate modifier. It describes call lowering, not context. An inline fn Name() uses the caller's context and lowers directly in the caller path instead of becoming a separate scheduled callee. Depending on the helper shape, the compiler may emit a direct architecture-local helper call or expand the helper statements. An inline fn Name[]() is a context-free helper whose call lowers directly to the generated package-level helper path.

static is also a separate axis. A static fn Name() belongs to a type or class instead of a component instance, but it is still scheduled unless it uses []. The compiler generates a caller-local worker with the normal run/busy/return protocol. A static inline fn Name() is lowered directly at the call site without scheduled static worker protocol, while static fn Name[]() and static inline fn Name[]() are context-free static helpers lowered through the package-helper function/procedure path.

The compiler rejects context-free and inline helper bodies that require scheduled hardware behavior, such as state {}, sync, waits, scheduled calls, subcomponent calls, iterator foreach, or recursive helper cycles. For more detail, see combinational-helpers-and-inline-functions.md.

Private and Public Functions

Functions are private by default. A private function is an implementation helper:

livt
component ByteClassifier
{
    fn IsUpperHex(value: byte) bool
    {
        return value >= 0x41 && value <= 0x46
    }

    public fn IsHexDigit(value: byte) bool
    {
        if (value >= 0x30 && value <= 0x39)
        {
            return true
        }

        return this.IsUpperHex(value)
    }
}

IsUpperHex is private because callers do not need to know how the classifier is implemented. IsHexDigit is public because it is part of the component's API.

Function Parameters

Parameters are inputs by default. Use out when a function needs to write a value back to the caller:

livt
component Queue
{
    public fn TryPop(value: out byte) bool
    {
        value = 0x00
        return false
    }
}

The exact implementation is not important here. The signature says the function returns true or false, and when it succeeds it can write the popped byte into the out parameter.

Callers must pass something assignable to an out parameter, such as a variable, field, or array element. A literal or computed expression cannot receive the written value.

Interfaces

Interfaces define contracts. They let code depend on what a component provides, not on how that component is implemented.

livt
interface IByteSource
{
    fn HasData() bool
    fn Read() byte
}

Any component that implements IByteSource must provide those functions:

livt
component ConstantByteSource : IByteSource
{
    override fn HasData() bool
    {
        return true
    }

    override fn Read() byte
    {
        return 0x41
    }
}

The override keyword marks a function as an implementation of an interface contract. That makes the component easier to review: the reader can tell which functions are public API choices and which ones satisfy an external contract.

Use override only for functions that implement an inherited interface contract. Interfaces contain function signatures and signal fields; constructors and processes belong to components, not interfaces.

Interfaces with Signal Fields

Interfaces can also group related signals. This is useful for buses, streams, and protocol boundaries:

livt
interface IByteStream
{
    valid: in logic
    ready: out logic
    data: in byte
}

Interface signal fields should use explicit directions. A component receiving IByteStream reads valid and data, and drives ready.

livt
component StreamConsumer
{
    stream: IByteStream

    new(stream: IByteStream)
    {
        this.stream = stream
    }

    process Consume[]()
    {
        if (this.stream.valid == 0b1)
        {
            this.stream.ready = 0b1
        }
        else
        {
            this.stream.ready = 0b0
        }
    }
}

This style keeps the signal bundle together instead of passing valid, ready, and data as unrelated constructor parameters.

When a component implements a signal interface with :, the interface's signal fields are automatically available on the component. The component does not need to redeclare them:

livt
interface IStream32
{
    valid: out logic
    ready: in logic
    data:  out logic[32]
}

component StreamProducerCore : IStream32
{
    process Produce[]()        // this.valid, this.ready, this.data are provided
    {                          // by the interface — no redeclaration needed
        this.valid = 0b1
        this.data  = 0x00000042
    }
}

Interface fields are contract fields. Implementing an interface is a commitment to provide them.

Flipped Interface Parameters

Interfaces are usually declared from the consumer's point of view. Sometimes a component is on the producer side of the same interface. In that case, use flip on the constructor parameter:

livt
component StreamProducer
{
    stream: IByteStream

    new(stream: flip IByteStream)
    {
        this.stream = stream
    }

    process Produce[]()
    {
        this.stream.valid = 0b1
        this.stream.data = 0x41
    }
}

flip IByteStream means the directions are reversed for this component. The producer drives valid and data, while the consumer drives ready.

Use flip only on constructor parameters. For primitive parameters, out still means a scalar value is driven outward by the component. Chapter 6 discusses producer and consumer interface relationships in more depth.

Interface Fields Without Direction

For now, interface fields that represent signals should always write in, out, or inout. A field without a direction is intended to become a stored public contract field in the future, but that behavior is not the normal way to describe signal bundles today.

In reader-facing code, prefer this:

livt
interface IStatus
{
    value: out byte
}

Do not use a directionless interface field as shorthand for an input signal.

Hardware Meaning

  • A component is a design boundary with generated ports, state, and behavior.
  • Constructor statements establish static structure and wiring; they are not a startup routine that runs after power-on.
  • Stored fields require hardware state, while signal fields expose directed communication.
  • Processes exist concurrently. Source order does not make one process run before another.
  • Normal functions use a scheduled call/return path; context-free fn[] and inline fn helpers have different lowering.

Common Mistakes

  • Treating a constructor as reset or initialization behavior.
  • Reading a public signal without considering direction and ownership.
  • Assuming processes execute sequentially because they are written sequentially.
  • Exposing internal state publicly instead of defining a small contract.

Summary

The building blocks of Livt are small, but they carry precise meaning:

  • Namespaces organize code.
  • Components define hardware design boundaries.
  • Private fields store implementation state.
  • Public signal fields expose directed hardware signals.
  • Public stored fields expose component-owned state.
  • Constants name fixed values and data.
  • Constructors wire components to signals, interfaces, and subcomponents.
  • Processes describe ongoing combinational or sequential behavior.
  • Functions provide scheduled APIs, context-free helpers, and inline helper lowering.
  • Interfaces define contracts between components.

As you read the rest of the book, keep one question in mind: what boundary is this code defining? Livt is most effective when components have clear responsibilities, fields have clear ownership, and interfaces describe real contracts rather than accidental collections of signals.

The next chapter turns to data: primitive types, arrays, literals, operators, and casts.