System design

Interfaces & Composition

Contract-first design, interface direction, flip parameters, pass-through wiring, and reusable component composition.

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:

livt
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.

livt
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:

livt
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.

livt
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:

livt
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:

livt
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:

livt
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:

livt
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:

livt
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:

livt
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:

livt
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.