Livt borrows useful ideas from object-oriented programming, but it applies them to hardware design. Components own state and behavior. Interfaces define contracts. Composition builds larger systems from smaller parts.
The goal is not to force hardware into a software class hierarchy. The goal is to make boundaries explicit and reusable.
Contract-First Design
An interface describes what a component provides:
interface IByteSource
{
fn HasData() bool
fn Read() byte
}
Code that depends on IByteSource does not need to know whether the bytes come from a UART, a ROM, a test fixture, or a packet buffer.
component HeaderReader
{
source: IByteSource
new(source: IByteSource)
{
this.source = source
}
public fn CanReadHeader() bool
{
return this.source.HasData()
}
}
This is the main benefit of interfaces: callers depend on a contract, not an implementation.
Implementing an Interface
A component implements an interface with : and marks the required functions with override:
component ConstantByteSource : IByteSource
{
override fn HasData() bool
{
return true
}
override fn Read() byte
{
return 0x41
}
}
The override keyword is useful documentation. It tells the reader that the function exists to satisfy a contract.
Designing with Hierarchies
Hierarchy is useful when several components share the same role or contract. In Livt, that usually starts with an interface: the interface defines what callers can rely on, and each implementation provides the behavior behind that contract.
interface IByteSource
{
fn HasData() bool
fn Read() byte
}
component ConstantByteSource : IByteSource
{
override fn HasData() bool
{
return true
}
override fn Read() byte
{
return 0x41
}
}
This style is close to object-oriented design: code can depend on IByteSource, while different components provide different implementations.
Advantages of a hierarchy-oriented design:
- Callers can use a common contract without knowing the concrete component.
- Test doubles and alternate implementations are easy to provide.
- Shared concepts are named once and reused consistently.
- APIs become easier to document because the contract is explicit.
Disadvantages to watch for:
- Deep hierarchies can hide where signals and state really live.
- A broad base contract can become hard to change once many components depend on it.
- In hardware design, timing, reset behavior, and ownership still need to stay visible; a type relationship does not replace those design decisions.
Use hierarchy when you want substitutability: several components can stand in for the same role, and callers should not need to know which implementation they receive.
Designing with Composition
Composition is useful when a component owns other components and wires them into a larger behavior:
component PacketPipeline
{
parser: HeaderReader
stats: PacketStatistics
new(source: IByteSource)
{
this.parser = new HeaderReader(source)
this.stats = new PacketStatistics()
}
}
The pipeline owns its parts and wires them together. Each subcomponent keeps its own responsibility, while the parent defines how data and control move between them.
Advantages of composition:
- Ownership is explicit: the parent component creates and wires its parts.
- Component boundaries remain easy to inspect in generated VHDL.
- Each subcomponent can be tested independently.
- Larger designs can be built incrementally from smaller, focused units.
Disadvantages to watch for:
- Wiring code can become repetitive if interfaces are not used well.
- A parent component can grow too large if it coordinates too many unrelated responsibilities.
- Composition does not automatically provide substitutability; use interfaces when callers should accept multiple implementations.
Use composition when you want ownership and structure: one component contains and coordinates other components. Use hierarchy when you want a shared contract with interchangeable implementations. In real Livt systems, both approaches are valid and often appear together.
Signal Bundles
Interfaces are also useful for grouping signals:
interface IByteStream
{
valid: in logic
ready: out logic
data: in byte
}
This keeps protocol signals together. Instead of passing valid, ready, and data through every constructor separately, a component can accept one stream interface:
component StreamConsumer
{
stream: IByteStream
new(stream: IByteStream)
{
this.stream = stream
}
}
The interface communicates that these signals belong to one contract.
Interface Direction
Declare signal interfaces from one consistent point of view. For many protocols, the consumer or slave perspective is the clearest default:
interface IByteStream
{
valid: in logic
ready: out logic
data: in byte
}
A consumer receives valid and data, and drives ready.
flip Constructor Parameters
A producer is on the opposite side of the same interface. Use flip when a component should drive the fields that the consumer normally receives:
component StreamProducer
{
stream: IByteStream
new(stream: flip IByteStream)
{
this.stream = stream
}
process Produce[]()
{
this.stream.valid = 0b1
this.stream.data = 0x41
}
}
flip IByteStream reverses the field directions for this constructor binding. It is more explicit than overloading out for an entire interface.
Pass-Through Wiring
Sometimes a parent component receives an interface and forwards it to a subcomponent:
component StreamStage
{
consumer: StreamConsumer
new(stream: IByteStream)
{
this.consumer = new StreamConsumer(stream)
}
}
This is pass-through wiring. The parent does not store or transform individual signals; it routes the interface to the subcomponent. Keep pass-through code simple and direct so the structure is obvious.
Test Doubles
Interfaces make tests easier. A test can provide a small implementation of an interface instead of instantiating a full subsystem:
component TestByteSource : IByteSource
{
override fn HasData() bool
{
return true
}
override fn Read() byte
{
return 0x42
}
}
This lets you test a consumer component with predictable input.
Summary
Use interfaces to describe contracts, hierarchy to model interchangeable roles, and composition to assemble systems from owned parts. Keep interface directions explicit, use flip for the opposite side of a signal bundle, and prefer small test implementations when verifying components in isolation.