tin  1.5.9
runtime

Purpose

runtime is the execution layer inside tin. It is the part of the framework that moves typed work through bounded storage, wakes cooperative tasks, advances integer-tick timers, and records simple dispatch metadata.

The runtime layer answers mechanical questions:

  • Where does this event or sample wait before it is processed?
  • How many values can be buffered?
  • What happens when the buffer is full?
  • Who is allowed to send, receive, or observe the latest value?
  • Which task wakes next?
  • Which delayed work is due at this tick?
  • What tick and sequence number describe this dispatch?

Those questions show up in embedded control, host simulation, trading systems, test replay, middleware adapters, and application servers. The runtime APIs do not know the product domain. They operate on C++ types supplied by the application.

Mental Model

Think of runtime as a small set of deterministic building blocks:

  • a bounded inbox for local typed values;
  • explicit rules for overflow;
  • handles that split send and receive authority;
  • actors that compose independent bounded components;
  • typed ports and links between actors;
  • cooperative tasks that suspend without blocking an OS thread;
  • timer queues driven by integer ticks;
  • source and sink concepts for adapters;
  • metadata that can be used by replay, tracing, and tests.

The application decides what values mean. runtime decides how those values are stored, moved, woken, and counted.

Relationship To Other Surfaces

tsm uses runtime policies when state-machine events need queues, delayed delivery, executors, or resource accounting. The state-machine definition still owns the legal behavior: states, events, guards, actions, history, and transition rules.

tio describes portable I/O-shaped contracts. A tio ADC or GPIO facade can publish samples or events into a runtime channel.

thal binds runtime work to a target. A thal adapter decides how interrupts, threads, OS wakeups, monotonic clocks, or hardware ticks become calls into runtime primitives.

Application and domain layers own product concepts such as drive state, market-data books, simulation models, schemas, exchange connectivity, robot models, or middleware graph policy.

API Entry Points

  • tin/runtime.h for concrete tin runtime primitives and bounded queue policy details.
  • tin::queue<T, StoragePolicy, OverflowPolicy> for explicit FIFO storage.
  • tin::queue_policy<StoragePolicy, OverflowPolicy> for runtime queue configuration.
  • tin::static_storage<N> and tin::target_storage<N> for fixed queue storage policies.
  • tin::channel<T, Capacity, OverflowPolicy> for bounded local value movement.
  • tin::sender<T>, tin::receiver<T>, and tin::latest_reader<T> for non-owning channel views.
  • tin::overflow::reject_newest, tin::overflow::drop_oldest, and tin::overflow::overwrite_latest for explicit full-buffer behavior.
  • tin::dispatch_context for plain tick/sequence metadata.
  • tin::tick_duration, tin::tick_count, and tin::ticks(n) for semantic tick values used by timers, coroutine sleeps, and dispatch metadata.
  • tin::event_sink<Sink, Event> and tin::event_source<Source, Event> for adapter-facing contracts.
  • tin::actor_like<T>, tin::actor_sink<T, Event>, and tin::actor_source<T, Event> for deterministic component composition.
  • tin::input_port<Event, Sink> and tin::output_port<Event, Source> for typed actor wiring.
  • tin::actor_link<Source, Sink> and tin::actor_group<Actors...> for explicit local composition.
  • coroutines for static task declarations, awaitables, executor-driven wakeups, tick sleeps, cancellation, and task resource accounting.

Queues

A queue is the explicit FIFO storage primitive. It combines a storage policy with an overflow policy for code that owns direct push/pop access.

#include "tin/runtime.h"
using Samples =
tin::queue<int,
tin::static_storage<8>,
Samples samples;
samples.try_push(1);
samples.try_push(2);
int value{};
samples.try_pop(value);
::tsm::overflow::drop_oldest drop_oldest
Definition: concepts.h:88
Tin runtime-kernel facade.

static_storage<N> is portable inline storage. target_storage<N> keeps the same public type while allowing a platform header to select a target-specific queue backend.

Channels

A channel is fixed-capacity storage for local typed values. Its capacity and overflow policy are part of the type.

#include "tin/runtime.h"
struct EnableOperation {};
using CommandChannel =
CommandChannel commands;
bool request_enable() {
return commands.try_send(EnableOperation{});
}
bool take_command(EnableOperation& command) {
return commands.try_receive(command);
}
Definition: sync.h:507
bool try_send(T const &value)
Definition: sync.h:517

The default overflow policy is reject_newest: when the channel is full, the new value is rejected and existing values stay queued.

Overflow Policy

Different systems want different full-buffer behavior. Runtime makes the choice visible in the channel type.

#include <cstdint>
#include "tin/runtime.h"
struct PriceLevel {
std::uint32_t instrument_id{};
std::int64_t price_ticks{};
};
using MarketDataQueue =
tin::channel<PriceLevel,
64,

drop_oldest keeps accepting new values by discarding the oldest queued value. That can be a reasonable policy for high-rate streams where stale data is less useful than fresh data.

Latest-sample channels use overwrite_latest:

#include <cstdint>
#include "tin/runtime.h"
struct PositionSample {
std::int32_t encoder_ticks{};
};
using PositionChannel =
tin::channel<PositionSample,
1,
PositionChannel position;
void encoder_isr(std::int32_t ticks) {
(void)position.try_send_from_isr(PositionSample{ .encoder_ticks = ticks });
}
::tsm::overflow::overwrite_latest overwrite_latest
Definition: concepts.h:89
constexpr tick_count ticks(tick_rep value) noexcept
Definition: ticks.h:85

overwrite_latest is for "current value" streams. Consumers that need FIFO history should use reject_newest or drop_oldest with a larger capacity.

Handles

Channels can produce non-owning views. This lets one part of a program send without receiving, another part receive without sending, and another part read the latest accepted value without consuming FIFO state.

auto tx = position.sender();
auto rx = position.receiver();
auto latest = position.latest_reader();
(void)tx.try_send(PositionSample{ .encoder_ticks = 42 });
PositionSample next{};
(void)rx.try_receive(next);
PositionSample observed{};
(void)latest.latest(observed);

The channel still owns the storage. The handles only expose narrower authority.

Actors

An actor is a component that runtime can advance cooperatively. It may own a queue, wrap an HSM runtime that has a queue, read from a driver adapter, produce replay records, or consume reports.

The actor surface answers one scheduling question: if the caller gives this component a turn, can it make bounded progress now? The actor API does not create threads, register callbacks, own the execution loop, or prescribe middleware names.

The basic actor shape is:

struct MyActor {
bool step();
std::size_t drain();
bool empty() const;
std::size_t pending_events() const;
};
static_assert(tin::actor_like<MyActor>);

step() performs at most one unit of immediately ready work. drain() repeats that local work until the actor has no more immediate progress to make. empty() and pending_events() expose backlog without requiring instrumentation or heap-backed queues.

Queued HSM runtimes already satisfy this shape:

using DriveRuntime =
static_assert(tin::actor_like<DriveRuntime>);
static_assert(tin::actor_sink<DriveRuntime, PowerDrive::Enable>);
Runtime< Definition, runtime_policy< dispatch_model::queued, queue_policy< target_storage< Capacity >, Overflow > >> queued_runtime
Definition: runtime.h:168

This matters once a system has more than one independent runtime component. A controller can be an HSM actor, a sensor path can be a channel-backed actor, and a host replay adapter can be another actor. The caller still owns the execution loop. See actor tutorial for a complete source-to-HSM-to-sink example.

Actor Ports

Ports are non-owning typed views. An input port can send one event type into a sink. An output port can receive one event type from a source.

struct Sample {
int value{};
};
struct SampleSource {
using event_type = Sample;
bool try_receive(Sample& sample) {
return samples.try_receive(sample);
}
};
SampleSource source;
DriveRuntime drive;
auto output = tin::make_output_port<Sample>(source);
auto input = tin::make_input_port<Sample>(drive);

The source and sink still own their queues and state. The ports only expose the authority needed by a link. This keeps driver code, replay code, behavior code, and reporting code from receiving more control surface than they need.

Actor Links

An actor link moves typed values from one output port to one input port. It moves at most one value per step().

auto sample_to_drive = tin::actor_link{ output, input };
if (sample_to_drive.step()) {
// One Sample reached the drive runtime queue.
}
actor_link(Source &, Sink &) -> actor_link< Source, Sink >

The link has one pending slot. If it receives a value from the source but the destination rejects it because its bounded queue is full, the link keeps that value and retries it on a later step(). That makes overflow behavior explicit: the sink still rejects admission, and the link prevents accidental loss between the source and sink.

Use link_all(link_a, link_b, ...) when a superloop wants to advance several links in declaration order.

Actor Groups

An actor group is a deterministic stepping view over several actors. It does not own actors; it stores references to actors owned by the application.

SensorActor sensor;
DriveRuntime drive;
ReporterActor reporter;
tin::actor_group actors{ sensor, drive, reporter };
while (!actors.empty()) {
(void)sample_to_drive.step();
(void)actors.step();
(void)drive_to_reporter.step();
}
actor_group(Actors &...) -> actor_group< Actors... >

actor_group::step() visits actors in declaration order and gives each actor at most one unit of work. actor_group::drain() repeats that process until no actor reports progress. This is useful for tests, bare-metal superloops, simulators, and host event loops because the scheduling order is visible in C++ and reviewable.

runtime_group remains available for the narrower case where every component is a runtime object. actor_group is the more general composition surface.

Actor Resource Accounting

Actor resources aggregate the same bounded storage facts used by runtime resource manifests.

using Resources =
tin::actor_group_resources<SensorActor, DriveRuntime, ReporterActor>;
constexpr auto snapshot = Resources::snapshot();
static_assert(!snapshot.uses_heap);

HSM runtimes contribute their existing runtime_resources snapshot. Generic actors can provide:

runtime::resource_snapshot resource_snapshot
Definition: runtime.h:131

Actors without a resource snapshot contribute zero. That default keeps the composition API open for small direct actors while allowing production actors to publish precise queue, timer, task, and heap-use accounting.

Actor Usage Pattern

Use actors when a system has multiple independently stepped components:

  • source actors ingest samples, records, or commands into bounded storage;
  • HSM runtime actors own reviewed behavior and event queues;
  • adapter actors translate platform or transport records into typed events;
  • sink actors collect reports, telemetry, or outbound commands;
  • actor links make typed handoff points explicit;
  • actor groups provide deterministic local scheduling.

The actor layer is the bridge between primitives and applications. Channels store values. HSMs define legal behavior. Executors decide when work runs. Actors describe how several bounded components form one local system.

Tasks And Timers

Runtime also provides cooperative task and timer primitives. Tasks can suspend on runtime operations and resume later through an executor. Timers are driven by integer ticks supplied by the application or platform adapter.

The first-class task guide is coroutines. This runtime reference keeps the mechanical timer and wakeup role visible; coroutine authoring, execution loops, task groups, cancellation, and resource accounting live on the coroutine page.

#include <cstdint>
#include "tin/runtime.h"
struct PollInputs {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
while (true) {
co_await tsm::after_ticks(1);
(void)runtime;
}
}
};
Definition: coroutine.h:196
Definition: coroutine.h:43
detail::runtime_impl< Definition, Policy, MachinePolicy > Runtime
Definition: runtime.h:531
sleep_ticks_awaitable after_ticks(tsm::tick_rep ticks) noexcept
Definition: coroutine.h:638

The runtime timer queue does not read a wall clock. A host test, bare-metal SysTick adapter, RTOS tick hook, or simulator advances ticks explicitly.

Adapter Concepts

Adapters use small concepts for event input and output.

#include "tin/runtime.h"
struct RiskRejected {};
template<typename Runtime>
requires tin::event_sink<Runtime, RiskRejected>
void publish_risk_rejection(Runtime& runtime) {
(void)runtime.send_event(RiskRejected{});
}
requires(!has_transition_type_c< T > &&has_transition_member_c< T >) struct transitions_of< T >
Definition: transition.h:479

The concept only says that runtime.send_event(event) returns something convertible to bool. Ownership, threading, transport, and retry policy belong to the adapter or application.

Dispatch Context

dispatch_context is plain metadata for deterministic dispatch, tracing, and replay.

The tick count is a std::chrono::duration type, not a clock. It can count up and down without binding the runtime to wall time, operating-system clocks, or chrono clock sources. The default representation is std::uint32_t and the default period is std::ratio<1>.

Projects with microsecond ticks, long-running simulations, or high-frequency host systems can compile the framework with a wider unsigned representation and an explicit period, for example -DTSM_TICK_REP=std::uint64_t -DTSM_TICK_PERIOD=std::micro. The same tin::tick_duration type is used by tin::tick_count, dispatch metadata, logging, and replay records. Timer queues still operate on integer tick counts and do not read clocks.

#include "tin/runtime.h"
struct RuntimeRecord {
tin::dispatch_context context{};
unsigned event_id{};
};
RuntimeRecord make_record(unsigned event_id,
tin::tick_count tick,
std::uint32_t sequence) {
return RuntimeRecord{
.context = tin::dispatch_context{
.tick = tick.value,
.sequence = sequence,
},
.event_id = event_id,
};
}

It does not identify nodes, topics, files, operating-system handles, or transports. Higher layers can embed it in their own records.

Typical Flow

A complete runtime path usually looks like this:

  1. A platform, driver, middleware, simulator, exchange, or replay adapter receives external input.
  2. The adapter validates or decodes that input into a local C++ type.
  3. The adapter sends the value into a runtime channel or event sink.
  4. A receiver, task, executor, or state-machine runtime consumes the value.
  5. Overflow, wakeup, and ordering behavior follow the explicit runtime policy.

The important point is that the handoff is typed, bounded, and reviewable.