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:
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.
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:
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:
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:
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:
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:
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:
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.
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.
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:
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:
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:
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:
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.
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.
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.
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:
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.
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:
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:
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:
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[]andinline fnhelpers 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.