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 page 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:
namespace Livt.App
Everything declared in that file belongs to Livt.App. A file can then import another namespace with using:
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:
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:
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:
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:
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:
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:
component UartTiming
{
ticksPerBit: int
new()
{
this.ticksPerBit = UartConfig.DIVIDER
}
}
Constants can also hold fixed-size data:
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.
Constructors
A constructor describes how a component is created and wired. It is introduced with new:
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.
Subcomponents
A component can own another component as a field and instantiate it in the constructor:
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.
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:
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:
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:
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.
Functions
Functions describe reusable behavior that runs when called. They can compute values, update component state, or provide a public API.
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.
Private and Public Functions
Functions are private by default. A private function is an implementation helper:
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:
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.
Interfaces
Interfaces define contracts. They let code depend on what a component provides, not on how that component is implemented.
interface IByteSource
{
fn HasData() bool
fn Read() byte
}
Any component that implements IByteSource must provide those functions:
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.
Interfaces with Signal Fields
Interfaces can also group related signals. This is useful for buses, streams, and protocol boundaries:
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.
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.
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:
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.
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:
interface IStatus
{
value: out byte
}
Do not use a directionless interface field as shorthand for an input signal.
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 reusable calculations and public APIs.
- Interfaces define contracts between components.
As you read the rest of the guide, 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 page turns to data: primitive types, arrays, literals, operators, and casts.