Livt applications are built from a small set of language constructs that describe hardware structure, state, and behaviour. Components define design boundaries, interfaces establish contracts between them, and processes and functions express how they operate.
Many of these concepts will be familiar to software developers, but Livt gives them hardware-specific meaning. Components represent hardware structures, stored fields retain state, and processes describe concurrent behaviour. This chapter introduces the fundamental constructs and their relationships. The examples are intentionally small: they illustrate individual concepts rather than forming one complete project, and a few fragments omit surrounding declarations.
Namespaces
A namespace groups related declarations and avoids naming conflicts as a project grows. Most Livt files begin with a namespace declaration:
namespace Livt.App
Declarations in this file belong to Livt.App. To refer to declarations in another namespace by their short names, use using:
namespace Livt.App.Tests
using Livt.App
The using declaration makes accessible names available; it does not install a package or create hardware. External packages are managed through project dependencies.
Choose namespaces that reflect responsibility, such as application logic, protocol components, tests, and reusable libraries. File names do not determine qualified names. Livt also supports namespace blocks, and declarations outside a namespace belong to the global namespace. For larger projects, see Packages and dependencies.
Components
A component is the fundamental unit of hardware design in Livt. It brings together state, behaviour, and connections to other components within a defined boundary.
Components resemble software objects in some respects, but their lifetime is different: a component represents hardware established as part of the design, not an object allocated dynamically while an FPGA runs. A network application, for example, might compose distinct components for reception, buffering, protocol handling, and transmission.
Even an empty component is valid:
component PacketStatistics
{
}
In practice, components contain fields, constants, functions, processes, or other components. Prefer meaningful boundaries such as PacketStatistics, FrameParser, or RegisterBank, rather than a component wrapping a single boolean operation.
Fields
Fields describe state and connections owned by a component. A stored value may require registers or memory; a directed signal represents communication across a hardware boundary; a component or interface reference connects to existing hardware rather than copying its state.
The declaration determines both who can access a field and what kind of hardware relationship it expresses.
Private stored fields
A field without public is private to its component:
component PacketStatistics
{
acceptedCount: int
droppedCount: int
public fn Accept()
{
acceptedCount = acceptedCount + 1
}
public fn Drop()
{
droppedCount = droppedCount + 1
}
}
Here the counters are implementation details. Stored fields begin with their type's zero value unless explicitly initialised otherwise, and retain state between calls. Initialisation and reset establish starting conditions; subsequent changes follow the component's execution and clocking rules. Other components cannot access these private fields directly.
Public signal fields
A public field with a direction is part of a component's signal contract:
component ReadyValidMonitor
{
public valid: in logic
public ready: in logic
public transfer: out logic
}
in marks a signal driven by the caller and read by the component; out marks one driven by the component. Direction determines ownership of the connection, not whether it requires a register. A function or process can later describe how the component drives its outputs.
Public stored fields
A public field without a direction exposes component-owned state:
component PacketStatistics
{
public acceptedCount: int
public fn Accept()
{
acceptedCount = acceptedCount + 1
}
}
This is different from public acceptedCount: out int, which declares an output signal. Other components can observe the stored count, while the owner remains responsible for updating it.
Design guidance: Keep implementation state private. Expose a public stored field when observation is part of the contract, and prefer public functions when other components need to request state changes.
Constants
Constants name values that do not change, such as widths, addresses, protocol identifiers, and limits:
component UartConfig
{
public const CLOCK_HZ: int = 50000000
public const BAUD: int = 115200
public const DIVIDER: int = CLOCK_HZ / BAUD
}
A public constant can be used elsewhere:
component UartTiming
{
ticksPerBit: int
new()
{
ticksPerBit = UartConfig.DIVIDER
}
}
Constants can also contain fixed-size data:
component HttpText
{
public const CRLF: byte[2] = [0x0D, 0x0A]
public const OK: byte[] = "OK".Encode()
}
Prefer named constants to repeated numeric literals. Use types that communicate intent—for example, uint for non-negative sizes and counts. Components and interfaces are structural references, not constant data. See Types and Expressions for types, arrays, and conversions.
Constructors
A constructor describes how a component is configured, initialised, and connected. It uses new:
component StatusOutput
{
enabled: logic
public active: out logic
new(enabled: logic, active: out logic)
{
this.enabled = enabled
active = this.active
}
}
The constructor binds an incoming value to the component and exposes its output to the caller. Its purpose is to establish structure and initial values; ongoing operation belongs in functions and processes. Bounded setup calculations can also belong in a constructor.
To create a component, write new ComponentName(...). The expression ComponentName() without new is not construction syntax. A component without an explicit constructor has an implicit zero-argument constructor. A component has one construction contract, not overloaded constructors.
Constructor parameters
Parameters make the initial configuration and connections explicit. A parameter may share a name with the field it initialises; use this. to distinguish them:
component Buffer
{
depth: uint
new(depth: uint = 64)
{
this.depth = depth
}
}
new Buffer() uses the default value, while new Buffer(depth = 256) supplies a named argument. Default values belong on trailing input parameters; positional arguments may precede named ones, but not follow them.
Subcomponents and composition
A component can own another component and construct it in its constructor:
component PacketPipeline
{
stats: PacketStatistics
new()
{
stats = new PacketStatistics()
}
public fn AcceptPacket()
{
stats.Accept()
}
}
This is composition: a larger hardware system is assembled from smaller units with distinct responsibilities. The stats field refers to one statically established PacketStatistics instance; repeatedly calling AcceptPacket() does not construct new counters.
A component can also accept a reference to an already existing component:
component SharedStatisticsPipeline
{
stats: PacketStatistics
new(stats: PacketStatistics)
{
this.stats = stats
}
public fn AcceptPacket()
{
stats.Accept()
}
}
The parent design decides whether two pipelines own separate counters or share a single statistics component. Constructing a component establishes owned hardware structure; passing an existing reference connects to hardware that already exists. Neither action allocates an object dynamically at runtime.
Top-level boundaries
The top-level component forms the integration boundary of an IP block or complete board design. Its external signal contract should map clearly to generated HDL ports. FPGA pin assignments and physical constraints are handled separately by the vendor flow.
Within the component hierarchy, interfaces and component references support flexible composition. At the top level, make external clock, reset, and signal connections easy to identify.
Processes
Processes describe behaviour that is continually present in a component. Unlike functions, they are not invoked by callers: they exist as part of the hardware and run concurrently. Writing two processes in a particular source order does not cause one to execute before the other.
Livt distinguishes combinational processes from clocked processes.
Combinational processes
Empty context brackets indicate a process with no clock or reset context:
component ReadyValidMonitor
{
public valid: in logic
public ready: in logic
public transfer: out logic
process Update[]()
{
transfer = valid & ready
}
}
process Update[]() describes logic whose output depends directly on its current inputs. Assign all driven outputs on every relevant path to avoid unintended storage. Combinational logic does not add a scheduled clock step, but it still consumes resources and has propagation delay.
Clocked processes
A process without empty brackets uses its component's clock/reset context:
component PacketCounter
{
public count: int
process Count()
{
count = count + 1
}
}
This process updates persistent state over time. A short source body does not, on its own, promise one update per clock cycle. When exact cycle behaviour matters, Livt provides explicit state sequencing. See Execution Models for the distinction between scheduled behaviour and cycle control.
Contexts
Every component has a context for clocked behaviour. A component may bind an explicit clock and reset in its constructor:
component TimedCounter
{
public count: int
new(clk: clock, rst: reset)
{
context.clk = clk
context.rst = rst
}
process Count()
{
count = count + 1
}
}
Use process Name[]() for combinational behaviour and process Name() for clocked behaviour. The distinction makes timing intent visible in the source.
Functions
Functions encapsulate operations that other functions, processes, or components can call. They can calculate results, modify component state, or define a component's public API. Unlike processes, functions execute in response to calls rather than running continuously.
component ByteClassifier
{
public fn IsAsciiDigit(value: byte) bool
{
return value >= 0x30 && value <= 0x39
}
}
A return type appears after the parameter list. A function without a return type performs an action rather than returning a value.
Function execution models
An operation in hardware can produce a result through combinational logic or require several clock cycles. A small calculation might be expressed directly as logic, while a memory access or multi-step algorithm may need sequencing and control. Livt distinguishes these cases so developers can express the intended execution model without manually writing every control signal.
A normal function is scheduled, while a context-free helper describes combinational work without its own clocked sequence. These forms have different hardware meanings:
| Form | Purpose |
|---|---|
fn Name() | Context-bound function using a scheduled call/return protocol |
fn Name[]() | Context-free helper for combinational operations |
inline fn Name() | Context-bound helper lowered into the caller's path rather than through a separate scheduled call boundary |
static fn Name() | Scheduled function belonging to a type rather than an instance |
A context-free helper can express a small calculation without its own clocked sequence:
component ByteClassifier
{
fn IsAsciiDigit[](value: byte) bool
{
return value >= 0x30 && value <= 0x39
}
public fn Check(value: byte) bool
{
return IsAsciiDigit(value)
}
}
process Name[]() is always-present combinational component behaviour; fn Name[]() is callable combinational helper logic. A context-free helper cannot wait for a clocked operation or make scheduled calls.
A context-free helper without a return value can use an explicit output parameter:
component ResultHelpers
{
fn Fill[](value: out int)
{
value = 42
}
public fn Run() int
{
var result: int
Fill(result)
return result
}
}
inline controls how calls are lowered, not whether arbitrary work becomes instantaneous. Context-bound inline code can still involve scheduled activity, and inlining can duplicate logic. static concerns ownership: a static function belongs to a type and cannot access instance state through this. These mechanisms are independent, and more detailed restrictions belong in Functions and Execution Models.
Public and private functions
Functions are private by default. A private helper supports implementation; a public function is part of a component's contract:
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 IsUpperHex(value)
}
}
Callers should depend on the public behaviour rather than the private calculations that implement it.
Function parameters
Parameters are inputs by default. Use out when a function needs to write a result back to the caller:
component Queue
{
public fn TryPop(value: out byte) bool
{
value = 0x00
return false
}
}
Here TryPop reports success or failure as a Boolean and has a separate output for the popped value when successful. inout is used when a supported value must flow both into and out of a function. Output arguments must be writable values such as variables, fields, or array elements—not literals or computed expressions.
Component and interface parameters are structural references to hardware, not copies of dynamically allocated objects.
Interfaces
Interfaces define contracts independently of implementations. They allow one component to rely on a capability without depending on how another component provides it. This separation is central to substitution, testing, and reusable design.
Behavioural contracts
Consider a component that processes incoming bytes. In different applications, those bytes might come from a UART, a memory buffer, or a network interface. The processing component should depend on the behaviour it needs, rather than on a specific source implementation. An interface expresses that shared contract.
An interface can declare operations:
interface IByteSource
{
fn HasData() bool
fn Read() byte
}
A concrete component implements the contract:
component ConstantByteSource : IByteSource
{
override fn HasData() bool
{
return true
}
override fn Read() byte
{
return 0x41
}
}
override marks functions that fulfil an interface contract. An interface does not create an additional hardware instance: it describes what an implementing component provides. A component can be used through the interface wherever that contract is expected.
Mark an interface public when other projects should depend on it. Interfaces declare contracts; constructors and ongoing processes belong to concrete components.
Signal contracts
Interfaces can also group signals exchanged between hardware components. This is useful for streams, buses, and protocol connections:
interface IByteStream
{
valid: in logic
ready: out logic
data: in byte
}
This contract is written from the consumer's perspective: the consumer reads valid and data and drives ready.
component StreamConsumer
{
stream: IByteStream
new(stream: IByteStream)
{
this.stream = stream
}
process Consume[]()
{
if (stream.valid == 0b1)
{
stream.ready = 0b1
}
else
{
stream.ready = 0b0
}
}
}
Passing the interface as one value keeps related protocol signals together and makes the connection easier to review. Interface signal fields should always state their directions explicitly.
Automatic interface fields
When a component implements an interface, the interface's signal contract fields become available on that component without being declared again:
interface IStream32
{
valid: out logic
ready: in logic
data: out logic[32]
}
component StreamProducerCore : IStream32
{
process Produce[]()
{
valid = 0b1
data = 0x00000042
}
}
These are contract fields rather than an unrelated copy of the interface. They remain visible in the hardware boundary generated for the implementing component.
Producer and consumer views
flip reverses signal directions for the producer.A producer sits on the opposite side of the same stream contract. Its constructor uses flip:
component StreamProducer
{
stream: IByteStream
new(stream: flip IByteStream)
{
this.stream = stream
}
process Produce[]()
{
stream.valid = 0b1
stream.data = 0x41
}
}
The producer now drives valid and data, while the consumer drives ready. One interface describes the protocol; the constructor binding determines which side of it a component uses.
Use flip only on constructor parameters. For a primitive output parameter, out still expresses the outward direction of that individual signal. See Interfaces and Composition for larger examples.
Stored contract fields
An interface can also describe component-owned public state without a signal direction:
interface ICounter
{
count: int
fn Increment()
}
component UpCounter : ICounter
{
override fn Increment()
{
count = count + 1
}
}
The implementing component provides count as part of its contract. A caller can observe this state through an ICounter reference and request a change using Increment(). A directionless contract field is not shorthand for an input signal: use in, out, or inout for directed hardware signals.
Understanding the Hardware Model
The building blocks are small, but their hardware implications differ. Abstractions such as namespaces and interfaces organise a design without inherently allocating extra hardware; actual resource usage follows from the state, logic, and component instances that are implemented:
| Construct | Hardware meaning |
|---|---|
| Namespace | Organises declarations; does not itself create hardware |
| Component | Defines a hardware design boundary |
| Stored field | Represents state retained by a component |
| Directed signal field | Describes hardware communication and drive ownership |
| Constant | Names fixed values or data |
| Constructor | Establishes structure, wiring, and initial configuration |
| Process | Describes ongoing concurrent behaviour |
| Scheduled function | Describes a callable operation with execution control |
| Context-free function | Describes callable combinational helper logic |
| Interface | Defines a behavioural or signal contract independent of implementation |
Summary
Livt combines familiar programming constructs with explicit hardware meaning. Components provide structure, fields represent state and communication, processes and functions describe behaviour, and interfaces establish reusable contracts. Together they allow systems to grow through composition without requiring developers to repeatedly express low-level implementation machinery.
As you continue, keep one question in mind: what boundary is this code defining? Clear responsibilities, explicit ownership, and well-designed contracts provide the foundation for reusable FPGA applications.
Continue with Types and Expressions for primitive types, arrays, literals, operators, and casts.