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.

Timing Metadata and Tick Counts

A context also exposes stable timing values that routines can read through this.context. This keeps timing-sensitive code tied to the context the component actually runs in, rather than to a separate manual constant.

livt
component TimeoutCounter
{
    public fn TimeoutTicks() uint
    {
        return this.context.TicksFor(1us)
    }

    public fn ClockRate() uint
    {
        return this.context.ticksPerSecond
    }
}

Use this.context.TicksFor(duration) when a duration should become a number of clock ticks. The result is rounded up, so the requested duration is not shortened. At 100 MHz, this.context.TicksFor(1us) is 100. At 50 MHz, the same source expression is 50.

livt
var setupDelay = this.context.TicksFor(250ns)
var byteTimeout = this.context.TicksFor(10us)

Direct timing fields are available when the rate or period itself is part of a component API, assertion, or diagnostic:

  • this.context.ticksPerSecond is the context rate in ticks per second.
  • this.context.periodNs is the full period in nanoseconds.
  • this.context.highTimeNs is the high time in nanoseconds.
  • this.context.lowTimeNs is the low time in nanoseconds.

Most application code should prefer TicksFor(...) over deriving tick counts by hand.

Context-Derived Timing

Timing-sensitive reusable components should describe protocol durations and let the assigned context decide how many ticks are needed. This is useful for timers, debouncers, UART, SPI, I2C, and other components that should work at more than one clock frequency.

livt
component DelayTimer
{
    public fn WaitTicks() uint
    {
        return this.context.TicksFor(10us)
    }
}

Avoid hard-coded tick constants for values that depend on the selected clock:

livt
component DelayTimer
{
    const WAIT_TICKS = 1000
}

The constant is only correct for one clock rate. The context-based form remains reusable because the user of the component can decide which context it should run in:

livt
@Test
@Context(ClockFrequency=100MHz)
component DelayTimerTest
{
    @Context(ClockFrequency=50MHz)
    peripheralContext: IContext

    timer: DelayTimer

    new()
    {
        this.timer = new DelayTimer()
        this.timer.context = this.peripheralContext
    }

    @Test
    fn UsesAssignedContext()
    {
        assert this.timer.WaitTicks() == 0n500
    }
}

If timer.context is not assigned explicitly, timer uses the surrounding component context. If it is assigned, timing follows the assigned context.

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.