tin  1.5.9
tsm

Purpose

tsm is the behavior layer. It turns C++ state-machine definitions into deterministic runtime behavior with typed states, events, transitions, guards, actions, hierarchy, history, orthogonal regions, and synchronization.

Use tsm for:

  • declaring legal product behavior;
  • separating reviewed behavior definitions from mutable runtime context;
  • dispatching typed events through tsm::hsm<Definition>;
  • selecting runtime policies for dispatch, queueing, overflow, and observation;
  • generating or inspecting behavior metadata.

API Entry Points

  • tsm.h for HSM definitions and the core authoring API.
  • tin/tsm.h when depending on the state-machine facade.
  • tsm::hsm<Definition> for the runtime state-machine object.
  • tsm::T and tsm::Ts for transition tables.
  • tsm::Runtime for explicit runtime policy composition.
  • tsm::cooperative_executor for caller-driven cooperative work.
  • coroutines for static tasks that sequence events, waits, sleeps, and supervision around an HSM runtime.

See authoring_model.md for the full definition/context model and production_api_surface.md for production API guidance.

Machine definitions can also flow through the .hsm DSL and verified-tsm.machine.v1 JSON for validation, diagrams, generated simulators, and integration scaffolds. See hsm_dsl_grammar, machine_ir_json, tooling, and generation_pipeline for those paths.

Definition And Context

Every tsm::hsm<Definition> has a context. The context is the mutable product data and side-effect endpoint used by guards, actions, entry methods, and exit methods.

For compact machines, the definition type itself is also the context type. That means the context accessor on tsm::hsm<PowerDrive> returns PowerDrive&.

For larger systems, the definition can name a separate context:

struct PowerDriveDefinition {
using context_type = PowerDriveContext;
// states, events, and transitions
};

In that form, the context accessor on tsm::hsm<PowerDriveDefinition> returns PowerDriveContext&. The definition remains the reviewed behavior artifact while the context owns counters, latched faults, service references, hardware adapter references, and side-effect endpoints.

Example: DS402 Power Drive State Machine

#include "tsm.h"
struct PowerDriveContext {};
struct PowerDrive {
using context_type = PowerDriveContext;
struct InternalReady {};
struct Shutdown {};
struct SwitchOn {};
struct DisableVoltage {};
struct QuickStop {};
struct EnableOperation {};
struct FaultDetected {};
struct FaultReactionComplete {};
struct FaultReset {};
struct NotReadyToSwitchOn {};
struct SwitchOnDisabled {};
struct ReadyToSwitchOn {};
struct SwitchedOn {};
struct OperationEnabled {};
struct QuickStopActive {};
struct FaultReactionActive {};
struct Fault {};
using transitions =
tsm::T<NotReadyToSwitchOn, InternalReady, SwitchOnDisabled>,
tsm::T<SwitchOnDisabled, Shutdown, ReadyToSwitchOn>,
tsm::T<ReadyToSwitchOn, SwitchOn, SwitchedOn>,
tsm::T<ReadyToSwitchOn, DisableVoltage, SwitchOnDisabled>,
tsm::T<SwitchedOn, EnableOperation, OperationEnabled>,
tsm::T<SwitchedOn, Shutdown, ReadyToSwitchOn>,
tsm::T<SwitchedOn, DisableVoltage, SwitchOnDisabled>,
tsm::T<OperationEnabled, SwitchOn, SwitchedOn>,
tsm::T<OperationEnabled, Shutdown, ReadyToSwitchOn>,
tsm::T<OperationEnabled, DisableVoltage, SwitchOnDisabled>,
tsm::T<OperationEnabled, QuickStop, QuickStopActive>,
tsm::T<QuickStopActive, EnableOperation, OperationEnabled>,
tsm::T<QuickStopActive, DisableVoltage, SwitchOnDisabled>,
tsm::T<SwitchOnDisabled, FaultDetected, FaultReactionActive>,
tsm::T<ReadyToSwitchOn, FaultDetected, FaultReactionActive>,
tsm::T<SwitchedOn, FaultDetected, FaultReactionActive>,
tsm::T<OperationEnabled, FaultDetected, FaultReactionActive>,
tsm::T<QuickStopActive, FaultDetected, FaultReactionActive>,
tsm::T<FaultReactionActive, FaultReactionComplete, Fault>,
tsm::T<Fault, FaultReset, SwitchOnDisabled>>;
};
tsm::hsm<PowerDrive> drive{};
drive.handle(PowerDrive::InternalReady{});
drive.handle(PowerDrive::Shutdown{});
drive.handle(PowerDrive::SwitchOn{});
drive.handle(PowerDrive::EnableOperation{});
consteval auto transitions(TransitionEntries...)
Definition: tsm.h:2448
transition_table< TransitionEntries... > Ts
Definition: tsm.h:2445

Runtime Configurations

The same state-machine definition can run with different runtime configurations. The definition says what behavior is legal. The runtime configuration says how events are delivered, queued, drained, delayed, and accounted for.

Use the simplest runtime that matches the deployment:

  • direct dispatch for synchronous logic and tight tests;
  • composite queues when events can arrive before the machine is ready to drain them;
  • overflow policies when full queues need explicit behavior;
  • per-region queues when orthogonal regions need separate back-pressure;
  • cooperative executors when HSMs run beside tasks;
  • tsm::app when one object should own the runtime, executor, and delayed event timers;
  • resource contracts when a target needs compile-time memory and queue budgets.

Direct Dispatch

Direct dispatch has no backlog. Sending an event immediately attempts to handle that event on the HSM.

using DriveRuntime = tsm::direct_runtime<PowerDrive>;
DriveRuntime runtime{};
(void)runtime.send_event(PowerDrive::InternalReady{});
(void)runtime.send_event(PowerDrive::Shutdown{});
Runtime< Definition, runtime_policy< dispatch_model::direct, queue_policy< static_storage< 2 > >> > direct_runtime
Definition: runtime.h:160

Use direct dispatch when the caller already owns ordering and back-pressure: unit tests, synchronous control loops, generated dispatch paths, and small single-threaded shells.

Composite Queue

A composite queue stores accepted events in one bounded FIFO. The application decides when to drain it.

using DriveRuntime =
DriveRuntime runtime{};
(void)runtime.send_event(PowerDrive::InternalReady{});
(void)runtime.send_event(PowerDrive::Shutdown{});
(void)runtime.step(); // dispatch one queued event
(void)runtime.drain(); // dispatch until the queue is empty
Runtime< Definition, runtime_policy< dispatch_model::queued, queue_policy< target_storage< Capacity >, Overflow > >> queued_runtime
Definition: runtime.h:168

Use a composite queue when interrupt adapters, driver tasks, middleware adapters, replay code, or host tests can enqueue work before the behavior loop runs.

Explicit Overflow

The default queued runtime rejects the newest event when full. Other policies can be selected explicitly:

using DriveRuntime =
tsm::queued_runtime<PowerDrive,
8,
DriveRuntime runtime{};
(void)runtime.send_event(PowerDrive::InternalReady{});
Definition: policy.h:40

reject_newest is usually the right policy for command and safety events because it preserves already accepted work. drop_oldest can fit high-rate streams where newer samples are more useful than stale samples. Latest-value data should usually enter through a tin::channel<T, 1, tin::overflow::overwrite_latest> and be translated into behavior events separately.

Full Policy Form

The shorthand aliases expand to tsm::Runtime plus a compile-time policy. Use the full form when the configuration should be visible in one place:

using DriveRuntime =
PowerDrive,
runtime::Runtime< Definition, Policy, MachinePolicy > Runtime
Definition: runtime.h:35
Definition: policy.h:29
Definition: policy.h:38
Definition: policy.h:52
Definition: policy.h:89

The policy axes are independent:

  • dispatch model: direct dispatch, one queued backlog, or per-region queues;
  • queue policy: target queue storage, capacity, and overflow behavior;

Per-Region Queues

Orthogonal machines can use per-region queues. Each region gets its own bounded queue, so pressure in one region does not consume every slot for the others.

using ArmSystem =
tsm::OrthogonalExecutionPolicy<ArmRegion, EnergyRegion>;
using ArmRuntime =
ArmRuntime runtime{};
(void)runtime.send_event<ArmRegion>(ArmRegion::Start{});
(void)runtime.send_event<EnergyRegion>(EnergyRegion::Enable{});
(void)runtime.step();
Runtime< Definition, runtime_policy< dispatch_model::per_region_queued, queue_policy< target_storage< Capacity >, Overflow > >> per_region_runtime
Definition: runtime.h:176

Per-region queues are useful when orthogonal regions represent independent logical subsystems, such as arm motion and energy control, or market-data ingest and order routing.

Executor-Driven Runtime

Executors drain ready runtime work and cooperative tasks. This shape is useful when behavior, driver-service tasks, watchdogs, or host adapters should run in one cooperative loop.

DriveRuntime runtime{};
tsm::cooperative_executor executor{ runtime };
(void)runtime.send_event(PowerDrive::InternalReady{});
(void)executor.run_ready();
Definition: runtime.h:45
std::size_t run_ready()
Definition: executor.h:172

The executor does not change the state-machine semantics. It changes who drains ready work and how coroutine tasks are resumed.

Runtime With Tasks

A machine definition can declare cooperative tasks. The runtime owns the HSM; the executor owns task scheduling. See coroutines for the full task model, wait primitives, tick sleeps, cancellation, dynamic static slots, and resource accounting.

struct EnableDrive {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
co_await tsm::send<PowerDrive::InternalReady>(runtime);
co_await tsm::send<PowerDrive::Shutdown>(runtime);
co_await tsm::send<PowerDrive::SwitchOn>(runtime);
co_await tsm::after_ticks(10);
co_await tsm::send<PowerDrive::EnableOperation>(runtime);
}
};
struct DriveApplication : PowerDrive {
using tasks =
};
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
Definition: coroutine.h:1046
Definition: coroutine.h:1143
Definition: coroutine.h:1162

Tasks are still statically declared. Frame budgets are explicit, which makes resource accounting possible for embedded targets and reviewable host builds.

App Composition

tsm::app is the convenience owner for a runtime, an executor, and optional delayed-event timer slots.

using DriveApp =
tsm::app<PowerDrive,
2>;
DriveApp app{};
(void)app.send_event(PowerDrive::InternalReady{});
(void)app.after_ticks(PowerDrive::FaultReactionComplete{}, 10);
app.tick(10);
Definition: app.h:34
cooperative_executor(Tasks &...) -> cooperative_executor< Tasks... >
runtime::app< Definition, RuntimePolicy, Executor, TimerSlots > app
Definition: app.h:165

Use app when an application shell wants one object that owns the machine, event queue, executor, and delayed event timers.

Resource Accounting

Runtime configurations expose compile-time resource summaries. Product code can turn those summaries into build-time contracts.

using DriveResources =
constexpr tsm::resource_budget budget{
.max_event_bytes = DriveResources::max_event_bytes,
.max_event_alignment = DriveResources::max_event_alignment,
.queue_slots = 8U,
.delayed_event_timer_slots = 2U,
.task_count = DriveResources::task_count,
.task_group_count = DriveResources::task_group_count,
.task_group_entry_count = DriveResources::task_group_entry_count,
.task_arena_bytes = DriveResources::task_arena_bytes,
.task_timer_slots = DriveResources::task_timer_slots,
};
using Contract =
Definition: resources.h:37
std::size_t max_event_bytes
Definition: resources.h:38
Definition: resources.h:148
Definition: resources.h:96

Resource contracts are useful when a profile requires bounded queue slots, bounded task frames, known delayed-event slots, and no heap-backed runtime storage.

Selection Guide

  • Use tsm::hsm<Definition> directly when the caller synchronously dispatches events and no runtime queue is needed.
  • Use tsm::direct_runtime<Definition> when code should use the runtime event sink shape but still dispatch immediately.
  • Use tsm::queued_runtime<Definition, N> when event producers and consumers are separated by time.
  • Use tsm::per_region_runtime<Definition, N> for orthogonal systems that need separate region queues.
  • Use tsm::app when the shell should own runtime, executor, tasks, and delayed event timers together.
  • Add runtime_resources and resource_contract when deployment limits should fail at compile time.

Related Pages