Timing boundaries

Contexts & Clock Domains

Use contexts to make timing boundaries explicit, and use sync where components communicate across those boundaries.

A Livt context describes the timing environment of a component. Most small designs use one context everywhere. Larger systems often need more than one: a fast system side, a slower peripheral side, or a test-only worker context.

Livt keeps those boundaries visible. Components can share a context, or a subcomponent can be placed in a different context. When one component communicates with another component across that boundary, mark the access with sync.

Component Contexts

When a subcomponent is constructed normally, it uses the same context as the component that owns it:

livt
component Counter
{
    public value: int

    process Count()
    {
        this.value = this.value + 1
    }
}

component App
{
    counter: Counter

    new()
    {
        this.counter = new Counter()
    }

    public fn ReadCounter() int
    {
        return this.counter.value
    }
}

Because App and Counter share the same context in this example, the public field access is ordinary component communication.

Separate Contexts

A component can also be assigned to another context. Tests often use this to exercise a boundary that is intentionally timed differently:

livt
component Worker
{
    public value: int

    public fn GetValue() int
    {
        return this.value
    }
}

@Test
@Context(ClockFrequency=100MHz)
component WorkerTest
{
    @Context(ClockFrequency=50MHz)
    workerContext: IContext

    worker: Worker

    new()
    {
        this.worker = new Worker()
        this.worker.context = this.workerContext
    }
}

The test component has a default 100 MHz context. workerContext is a separate 50 MHz context, so accesses from the test component to the worker cross a context boundary.

Context identity matters. Two contexts can use the same frequency and still be separate timing domains. If components use the same context reference, they are in the same domain.

The sync Marker

Use sync exactly where a cross-context access happens:

livt
var value = sync this.worker.GetValue()
var status = sync this.worker.status
sync this.worker.command = nextCommand

The marker belongs at the access site. The same worker function may be same-context in one design and cross-context in another, depending on how the components are connected.

Supported Accesses

Use sync for public component-boundary communication:

  • calling a public function on another component;
  • reading a public primitive field from another component;
  • writing a public primitive field on another component.

Do not use sync around local arithmetic, literals, or private implementation details. It is for communication between components, not ordinary expressions inside one component.

Status Reader Example

Keep the crossing at a clear component boundary. In this example, all cross-context communication is visible in the functions that read from the worker:

livt
component StatusWorker
{
    public ready: logic
    public status: byte

    public fn GetStatus() byte
    {
        return this.status
    }
}

component StatusReader
{
    worker: StatusWorker

    new(worker: StatusWorker)
    {
        this.worker = worker
    }

    public fn IsReady() bool
    {
        var ready = sync this.worker.ready
        return ready == 0b1
    }

    public fn ReadStatus() byte
    {
        return sync this.worker.GetStatus()
    }
}

Testing with Contexts

Add an extra context in a test when the timing boundary is part of the behavior you want to verify:

livt
@Test
@Context(ClockFrequency=100MHz)
component StatusReaderTest
{
    @Context(ClockFrequency=25MHz)
    workerContext: IContext

    worker: StatusWorker
    reader: StatusReader

    new()
    {
        this.worker = new StatusWorker()
        this.worker.context = this.workerContext
        this.reader = new StatusReader(this.worker)
    }

    @Test
    fn ReadsStatusAcrossContext()
    {
        var status = sync this.worker.GetStatus()
        assert status == 0x00
    }
}

Keep the fixture small: one default context, one named additional context, and one or two synchronized accesses are often enough to document the boundary.

Design Guidance

Use names such as sensorContext, busContext, or workerContext so the reason for the boundary is visible. Reserve sync for the places where a boundary is part of the architecture.

A small status value, command field, or function result is easier to reason about than a large bundle of unrelated state. For continuous high-throughput data flow, prefer a dedicated buffering or streaming component with its own tests.

Summary

A context is the timing environment of a component. Components normally share the owning context, but tests and larger systems can assign separate contexts where needed. Use sync at the exact public component access that crosses from one context to another.