Language reference

Execution Models

Choose how your Livt components calculate, respond to calls, and operate over time.

An execution model describes how behaviour participates in a Livt design. A function performs an operation when called; a process supplies ongoing behaviour within its component. Either can describe combinational logic or behaviour that progresses through a clocked sequence. Choosing the form makes the relationship between inputs, stored state, and results visible in the source.

This page develops a byte-processing example with a combinational inverter, a scheduled accumulator, and a recurring counter. A second component captures, transforms, and publishes a byte in three stages.

Choose an execution model

Start with two questions: is the behaviour invoked by a caller or continuously active, and does it calculate combinationally or progress over clock edges? Empty context brackets, [], select the context-free form. A function or process written without those brackets uses its component context.

four Livt execution forms
Invocation and timing are separate choices. Static selection and inlining are additional properties.
FormTypical use
fn Name(...)A finite scheduled operation that can update component state.
fn Name[](...)A callable combinational calculation.
process Name()Ongoing clocked behaviour or a recurring sequence.
process Name[]()Always-active combinational behaviour driven by inputs.

These forms work together. A scheduled operation can call a combinational helper, while a component’s processes continue their own work. A source statement is not automatically one cycle, and a short function is not automatically combinational. Choose from the required behaviour, then make any timing contract explicit and test it.

Describe a finite operation

A function begins with fn, followed by its name, parameters, and optional return type. Functions are private by default; public exposes an operation to callers outside the component. Use a return value when the caller needs a result, and omit the return type for an action with no returned value.

Wait for a scheduled result

The byte engine keeps a running total. Its public function receives a byte, updates the stored total, and returns the completed result:

livt
public fn Accumulate(value: byte) int {
    total += value as int
    return total
}

Because this declaration has no empty context brackets, the call participates in the component’s clocked execution. The caller starts the operation and waits for completion before using the result. Calling Accumulate(3) and then Accumulate(4) on a freshly initialised engine returns three and seven. The function can retain state across calls even though each call is a finite operation.

A scheduled function can contain several steps or call another scheduled operation. Its apparent size does not establish its latency. Keep private helpers for implementation details and public functions for the component contract; expose completion through the return value and parameter results rather than assuming a caller can observe an intermediate update immediately.

Calculate without a clocked sequence

Use a context-free function when a bounded calculation needs no clocked sequence. The empty brackets are part of the declaration, not the call syntax. This helper inverts the eight bits of a byte:

livt
fn Invert[](value: byte) byte {
    return value ^ 0xFF
}

Calling Invert(value) contributes combinational work to the calling behaviour. It does not add a scheduled request/completion interaction. The logic still has propagation delay, so context-free does not mean physically instantaneous. Loops and call chains must have statically bounded hardware meaning; waiting or invoking a scheduled operation belongs in a context-bound function or process.

Read values from the current instance

A non-static context-free helper can read primitive fields of its own component, including fixed-size arrays. The values visible at the call become read-only inputs to the calculation. For example, a component can expose its stored count through a scheduled public function and a local combinational helper:

livt
public fn ReadCount() int { return CountSnapshot() }
fn CountSnapshot[]() int { return count }

The helper can use count or this.count, but it cannot write the field, pass it as a writable argument, or traverse a component field. Call an instance context-free helper on that same instance. For reuse elsewhere, pass explicit values to a suitable static helper or expose a scheduled public operation. Static functions cannot read instance state.

context-free field-write diagnostic in VS Code
A context-free calculation cannot update stored instance state. Choose the execution form that matches the operation.

Describe ongoing behaviour

A process belongs to the active component rather than to an individual call. It is not invoked by another function and has no return value. Use its fields and interfaces to communicate with other hardware, and make ownership clear so each output has an intended writer.

Drive a result from the inputs

The byte engine uses its inverter in a combinational process. Its input is an incoming byte signal, and inverted is an outgoing byte signal:

livt
process Decode[]() {
    inverted = Invert(input)
}

The output follows the input calculation without a clocked storage stage. A combinational process must define its driven outputs on every path, including the alternatives of a condition. Use this form for continuously present behaviour and a context-free function for a reusable calculation called from that behaviour.

Repeat a clocked step

The counter has a stored integer field, ticks. Its single recurring state makes the intended update on each clock edge explicit:

livt
process Count() {
    state Tick {
        ticks++
        goto Tick
    }
}

The field persists between executions and follows the component’s reset configuration. The explicit return to Tick establishes this recurring step; a short process body alone is not a promise of a one-cycle repetition rate. See Project Configuration for reset settings and Control Flow for state syntax and transitions.

Keep concurrent work distinct

The decoder, counter, and scheduled accumulator coexist in the same component. Their position in the source file does not make them execute one after another. In this example, they have separate destinations: the decoder drives inverted, the counter updates ticks, and the accumulator owns total. Communication between concurrent operations needs an explicit signal or state contract.

Statements within one function or process still have an execution order. A scheduled caller waits for its called operation, while unrelated processes remain active. Two clients using the same component need a defined access protocol; concurrent hardware alone is not a promise that every operation accepts overlapping calls.

four execution forms together in the byte engine
The helper, scheduled operation, combinational process, and recurring clocked process have distinct roles in one component.

Follow a sequence over time

A named sequence is useful when the stages themselves are part of the design’s meaning. This component captures an input byte, inverts the captured value, and publishes the result. The intermediate fields retain their values between stages, and explicit transitions return the process to Capture:

livt
namespace Examples.Execution

component ByteSequence
{
    public input: in byte
    public sample: byte
    public adjusted: byte
    public result: byte
    public phase: int

    process Transform()
    {
        state Capture {
            sample = input
            phase = 1
            goto Calculate
        }
        state Calculate {
            adjusted = sample ^ 0xFF
            phase = 2
            goto Publish
        }
        state Publish {
            result = adjusted
            phase = 3
            goto Capture
        }
    }
}

The phase field identifies the stage for the example’s tests: one for Capture, two for Calculate, and three for Publish. It is an observation aid, not a separate scheduler. Once the process is running, the tests observe the captured sample at one edge, the adjusted value at the next, and the published result at the following edge.

three-stage capture calculate publish timing
The relative edges describe the running sequence. They do not include reset, startup, or an enclosing scheduled call.

For input 0x3C, the sequence captures 0x3C, computes 0xC3, and publishes 0xC3. Capture occurs again on the fourth labelled edge. This is a recurring three-step sequence: it accepts one sample every three edges, not a pipeline accepting a new sample on every edge.

An ordinary brace block groups statements and scope. A state groups ordinary non-suspending updates into a step, but scheduled calls, waits, and cross-context accesses can suspend it. Adding such work changes the timing assumptions of this example. Use Contexts for clock-domain interactions and Control Flow for branching and sequencing within an operation.

Pass values and return results

A parameter direction describes how values cross a call boundary. Scalar inputs carry values into a function. Output parameters return additional results, while inout parameters receive a starting value and return its update. For a scheduled call, the caller uses those completed results when the call finishes.

livt
public fn Copy(value: byte, result: out byte) {
    result = value
}

public fn Accumulate(value: inout int, addend: int) {
    value += addend
}

For example, Copy(0x3C, result) assigns the returned byte to result. Starting with total = 5, Accumulate(total, 7) leaves total equal to twelve. Output and inout arguments must be writable destinations; a literal, a computed expression, or an immutable whole binding cannot receive the update.

Parameter formValues supplied to the callValues returned to the caller
Scalar with omitted direction or inInput valueNone through that parameter
Array with inArray contentsNone through that parameter
Array with omitted directionArray contentsUpdated contents
outAn assignable result destinationAssigned result
inoutInitial value and assignable destinationUpdated result

An omitted array direction is writable, so it does not mean the same thing as an omitted scalar direction. Prefer explicit in for read-only arrays and inout when caller-visible updates are intended. Value transfer describes the call contract; it is not shared software memory. See Types and Expressions for array shapes and immutable bindings.

Refer to existing hardware

A component or interface argument refers to existing hardware rather than copying it. Field directions describe the provider: a caller can observe the provider’s output signals and drive its input signals through a writable structural parameter. For example, a sink can declare public data: in byte so a sender can drive data.

An explicit in structural parameter is read-only: it cannot write fields or be forwarded to a writable structural parameter. Undirected stored fields exposed through a borrowed argument are readable; use the provider’s operations to change its stored state. These rules also apply to compound updates and output arguments. flip belongs to constructor wiring, not function parameters. See Interfaces for these ownership contracts.

Provide useful defaults

Defaults make the common call concise while keeping alternatives explicit. A function may supply defaults for trailing input parameters:

livt
public fn ReadLimit(start: int, count: int = 16) int {
    return start + count
}

ReadLimit(4) returns twenty using the default count of sixteen, while ReadLimit(4, 32) returns thirty-six. With positional arguments, only a trailing suffix may be omitted. In the declaration, a required parameter cannot follow a defaulted one. Defaults belong on input parameters, not output or inout parameters that need a caller-supplied destination.

An interface can supply the default as part of its public contract. An implementation inherits it without repeating or replacing it; matching inherited defaults must agree. The example also demonstrates that the override can use a different local name:

livt
interface IAccumulator
{
    fn Add(increment: int = 1) int
}

// Inside CallContracts, which implements IAccumulator:
override fn Add(amount: int) int {
    count += amount
    return count
}

The caller writes contracts.Add() or contracts.Add(increment = 3). The implementation uses amount internally, but the interface still supplies the call name increment and its default value. This distinction matters when maintaining a public API.

Make call intent visible

Named arguments are useful for configuration values, booleans, or calls that retain some defaults. They use name = value and work for both functions and constructors:

livt
let standardLimit = contracts.ReadLimit(start = 4)
let customLimit = contracts.ReadLimit(count = 32, start = 4)

Positional arguments must precede named ones. Each parameter can be supplied only once, and every required parameter must be present. Named arguments can skip a defaulted parameter while providing a later one. Explicit argument expressions are evaluated once in the order written, including scheduled calls that complete before the next argument is evaluated; names do not reorder those effects into declaration order.

Naming an argument does not change its type, direction, constant requirement, or execution model. The names come from the callable contract, including an interface or callable-parameter signature. Renaming a public contract parameter can break named calls even though it does not create a different overload.

Select a suitable signature

Overloads let one operation name accept different parameter signatures. The compiler binds the arguments and selects a unique best candidate before supplying defaults. An exact type match is preferred to an implicit conversion; when conversions are otherwise equal, an exact-arity candidate is preferred to one requiring defaults.

livt
public fn Encode(value: byte) int { return value as int }
public fn Encode(value: int) int { return value }
public fn Encode(value: int, offset: int = 1) int {
    return value + offset
}

A byte argument selects the first overload. Encode(7) selects the one-parameter integer overload; Encode(7, 3) selects the two-parameter form. If no unique candidate is best, clarify the call with an appropriate cast or a less ambiguous API.

Overload identity includes the function name and each parameter’s type, array shape, and direction in declaration order. Return types, parameter names, defaults, and constant markers do not create distinct overloads. An override replaces the matching inherited signature while other overloads remain available. Its local parameter names may differ, but callers still use the inherited contract’s names.

Reuse and specialise behaviour

Static selection, inlining, and compile-time parameters describe different aspects of reuse. They are additional choices alongside the context-bound or context-free model. Apply them to a concrete architectural need rather than treating any modifier as a general promise of faster hardware.

Select a helper through its type

A static function belongs to its declaring type and cannot use instance state. Select it through that type, as in CallContracts.Sum(200, 100), which returns three hundred:

livt
public static fn Sum[](left: byte, right: byte) int {
    return (left as int) + (right as int)
}

Here the empty brackets make the calculation combinational. Static by itself does not do that: a static function without empty context brackets is still context-bound. Similarly, an instance function with empty brackets remains context-free.

Incorporate a helper into its caller

An inline helper contributes its body to the caller’s execution path. For scheduled code, this can remove the helper’s separate call boundary while preserving any scheduled operations inside it. A return completes the helper expansion, not the enclosing function. Inputs are evaluated once, and component or interface arguments retain their reference bindings.

Inlining does not eliminate the work. If an inline helper calls a scheduled reader, the reader’s request/completion interaction still occurs. Repeated expansion can duplicate logic or local storage, so use it for small helpers when direct incorporation serves the design.

Bind values during compilation

A private inline helper can mark a value parameter const when the caller must supply it as a static expression. This makes a structural choice explicit while ordinary parameters remain values supplied to each call:

livt
inline fn Scale(value: int, factor: const int = 2) int {
    return value * factor
}

public fn ScalePair() int {
    let first = Scale(5)
    let second = Scale(7, 3)
    return first + second
}

The default factor is two, and the second call explicitly selects three, giving a total of thirty-one. Separate locals make the scheduled helper calls’ evaluation points clear. The compiler specialises the helper for distinct constant argument tuples and reuses a specialisation for repeated calls with the same tuple. The constant marker does not create a separate overload.

Select an operation at compile time

A callable parameter is useful when a reusable sequence should run an operation chosen during compilation. A private context-bound function, or a source-backed static template, can accept a const callable with a complete signature. This example uses a private static helper and supplies a scheduled lambda directly:

livt
private static fn Apply(value: int, operation: const fn(value: int) int) int {
    return operation(value)
}

public fn AddAfterState(value: int, offset: int) int {
    return CallContracts.Apply(value, fn(current: int) int {
        state {}
        return current + offset
    })
}

The calls AddAfterState(5, 2) and AddAfterState(5, 4) return seven and nine. The offset is read from the enclosing call, while the empty state remains part of the lambda’s scheduled behaviour. Compile-time binding chooses the operation; it does not turn that operation into a combinational calculation.

A private instance or static receiver is local to its declaring component. A public static receiver can provide a reusable source template to other components or packages; its body must be available to the compiler, and it cannot read instance state. Public instance and interface functions represent runtime component contracts and do not accept these callable parameters.

The compiler specialises the operation without a runtime function pointer or closure object. Lambda parameters and locals belong to the lambda. Enclosing parameters and locals can be read while the call waits but cannot be written by the lambda. Lexically available component fields follow the ordinary function rules, including updates where allowed.

Verify and refine the design

Verify the property your interface promises. For the examples on this page, use functional tests to check combinational results, repeated accumulation, parameter updates, defaults, named calls, overloads, and specialised helpers. Use separate timing checks for the recurring counter and consecutive observations of Capture, Calculate, Publish, and the next Capture. For an instance named engine, the counter check can be written as follows:

livt
var before: int
state { before = engine.ticks }
state {}
state { assert engine.ticks == before + 2 }

The sample and assertion use explicit states so the observation itself has a defined position in the sequence. To test the three-stage sequence, first align with Capture, bound that alignment wait, then check each following stage on the next edge. Observing through an ordinary scheduled getter would add a call interaction and would not establish the same edge contract.

Add these checks to your project’s test component and run it from Livt Tests or Test Explorer. See Testing for creating and running a test component. Simulation checks observable behaviour; it does not measure routed timing or resource use.

six passing execution-model tests in VS Code
Functional checks and edge-sensitive checks verify different parts of the component contract.

Refine a design where its requirements demand it. A scheduled operation is convenient when a caller can wait for completion. A datapath that must accept a value on every eligible edge needs a signal-level acceptance contract and clocked updates that meet that rate. Keep reset priority, backpressure, and simultaneous acceptance and removal explicit when designing a queue or transfer slot.

Measure latency, throughput, resource use, and physical timing separately. Removing a scheduled boundary can lengthen a combinational path; inlining can duplicate logic; a smaller cycle count is not automatically a better implementation. Continue with Testing for observation and verification, and Hardware Integration for checking the generated design in the target toolchain.