The codegen now emits one `__chart_observe!` invocation per observable
atom. The macro fans out at compile time to whichever sinks are enabled:
the in-memory journal (`feature = "journal"`), `defmt` (`trace-defmt`),
`log` (`trace-log`), and/or `tracing` (`trace-tracing`). Trace and
journal share the same vocabulary by construction — they cannot
diverge. With no sink feature on, every site expands to `()` (zero
overhead). Multiple trace backends now compose; previously they were
mutually exclusive.
Trace lines use a logfmt-style format with a `[statechart:Name]` prefix
and `key=value` pairs, machine-parseable and greppable. Twenty verbs
cover every observable atom — entries/exits with begin/end pairs,
actions with kind, durings, transitions with the reason that triggered
them, event lifecycle (received/delivered/dropped/queued/failed),
timers, and termination.
BREAKING CHANGES (chart syntax unchanged; observability surface only):
- `TraceEvent::TransitionFired` now carries `reason: TransitionReason`
showing whether the transition was driven by an event, a timer, or
internal logic.
- New `TraceEvent` variants for phase pairing: `EnterBegan`, `ExitBegan`,
`TransitionComplete`, `EventReceived`.
- Trace prefix changed from `[Name]` to `[statechart:Name]`.
- The chart-level `trace;` keyword is parsed for backwards compatibility
but no longer gates emission.
- `__chart_trace!` / `__chart_journal!` macros removed (doc-hidden).
Verification: 159 integration tests pass, 0 missed mutants, miri 68
tests clean, Creusot 14/14 VCs discharged. Zero overhead confirmed via
`cargo expand` — every observation site expands to `{}` with no sinks
enabled.
9.7 KiB
Changelog
All notable changes to this workspace are documented here. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
0.4.0 — 2026-04-28
Changed — observability (breaking)
- Unified observation pipeline. The codegen now emits a single
__chart_observe!invocation per observable atom, dispatched at compile time to whichever sinks are enabled: the in-memory journal (feature = "journal"),defmt(trace-defmt),log(trace-log), and/ortracing(trace-tracing). All sinks share the same vocabulary by construction — the journal IS the observation stream; trace backends are textual renderings of it. With NO sink feature on, every call site expands to()(zero overhead). Multiple trace sinks may now be enabled simultaneously (previously they were mutually exclusive). - Trace prefix changed.
[ChartName] verb …→[statechart:ChartName] verb …. The new form is greppable and unambiguous in mixed log streams. - Trace format is now logfmt-style. Every line is
[statechart:Name] <verb> key=value key=value …(or native structured fields undertracing). Twenty verbs cover every TraceEvent variant — entries, exits (with begin/end pairs), actions (with kind), durings, transitions (with reason showing what triggered them), events (received/delivered/dropped/queued/failed), timers, terminate. trace;keyword deprecated. Still parsed for backwards compatibility, but no longer gates emission. Trace output is now controlled purely by whichtrace-*cargo feature you enable.
Changed — journal (breaking)
TraceEvent::TransitionFiredcarriesreason: TransitionReason. Replay consumers now see whether a transition was driven by an event (Event { event: u16 }), a timer (Timer { state, timer }), or internal logic (Internal).- New TraceEvent variants for phase begin/end pairing.
EnterBegan { state }fires before a state's entry actions / timers / durings;Enteredkeeps its place as the end marker.ExitBegan { state }fires before a state's during cancellations / timer cancellations / exit actions;Exitedkeeps its place as the end marker.TransitionComplete { from, to }fires after a transition's full exit-then-enter sequence finishes; pairs withTransitionFired.EventReceived { event }fires when an event is popped from the queue, before handler search; pairs withEventDelivered/EventDropped/TerminateRequested.
Added
hsmc/tests/trace_format.rs— 11 tests pinning every verb's exact textual rendering, including a journal/trace atom-count equivalence test.TransitionReasonenum — re-exported from the crate root whenfeature = "journal"is on. Carried onTraceEvent::TransitionFired.- Multiple trace backends compose. Previously enabling more than
one of
trace-defmt/trace-log/trace-tracingwas a compile error. Now they fan out independently.
Migration
Chart syntax is unchanged: every v0.1 / v0.2 / v0.3 statechart compiles under v0.4 without modification. Breakage is confined to the observability surface area:
-
Trace line prefix. Update grep patterns and log filters from
[ChartName]to[statechart:ChartName]. The verb after the prefix is now a single hyphenated token followed bykey=valuepairs. -
TraceEvent::TransitionFiredhas a newreasonfield. Update pattern matches:// before: TraceEvent::TransitionFired { from, to } => … // after: TraceEvent::TransitionFired { from, to, reason } => … // or, if you don't care: TraceEvent::TransitionFired { from, to, .. } => … -
New
TraceEventvariants. If youmatchexhaustively, add arms forEnterBegan,ExitBegan,TransitionComplete,EventReceived. Otherwise nothing changes. -
trace;keyword is now a no-op. Charts that declared it still compile. Trace output is now controlled purely by whichtrace-*cargo feature you enable. -
__chart_trace!and__chart_journal!macros are removed. These were doc-hidden and not part of the public API; mentioned here for codebases that grepped through the macro emission for patches/forks.
0.3.0 — 2026-04-27
Added
journalCargo feature. Records every observable atom of chart execution —Started,Entered,Exited,ActionInvoked(Entry/Exit/Handler),DuringStarted,DuringCancelled,TransitionFired,EventDelivered,EventDropped,EmitQueued,EmitFailed,TimerArmed,TimerCancelled,TimerFired,TerminateRequested,Terminated— into aVec<TraceEvent>on the generated machine. Same chart + same events = byte-identical journal. Public API:journal(),take_journal(),clear_journal(),CHART_HASH(a stable FNV-1a fingerprint of the chart's structural definition).- Root state targetable by chart name.
on(Trig) => MyChart;resolves to the root state in bothclassify_targetandresolve_transitions. From any nested state this is an up-transition (root is always on the active path). - 113 deterministic-flow tests (under
--features tokio,journal) comparing hand-built expected journals byte-for-byte against actual, anchored to spec sections: initial entry, lateral / self / up transitions, default-descent, event bubbling, timer arm / cancel / fire, emit, ordering, durings, step-vs-dispatch equivalence, root-targeting,current_state(), termination. verification/workspace member with Creusot proofs. Verified mirrors of the runtimeEventQueueandTimerTablecarrycreusot-std(Pearlite) contracts that Why3 + alt-ergo + z3 discharge mechanically. 14/14 VCs proven.just verifyis self-installing (mise pulls opam + the pinned nightly + Creusot's canonicalINSTALL).docs/002. hsmc-semantics-formal.md— canonical, unambiguous reference for chart behavior. Numbered S-rules supersede the v0.1 prose spec and v0.2 design-change-request.- README "How a chart behaves" section — eight building-block rules that build on each other, with an explicit "what hsmc deliberately does NOT have" table.
Tooling
mise.toml(root) declares stable Rust + components + embedded targets + cargo subcommands.verification/mise.tomldeclares the pinnednightly-2025-11-13+ opam + python venv + uv.set shellandset script-interpreterroute everyjustrecipe throughmise exec— no shebangs, no per-recipe wrappers.just verifyrunscargo creusot prove --no-cacheso SMT invocations happen on every run;timeshows ~7s CPU on ~1.2s wall = real parallel work.just mutantsruns with--features tokio,journalsojournal_*anddet_*tests execute. 0 missed mutants.just miriexcludeshsmc-macros(trybuild glob hits Miri's isolation) andhsmc-verification(different pinned toolchain). 66 tests pass under Miri, 0 UB.
Backward compatibility
Every v0.1 / v0.2 statechart compiles under v0.3 unchanged.
0.2.0 — 2026-04-22
Fixed — behavior-visible (breaking)
- Up-transitions no longer exit and re-enter the target ancestor.
A transition whose target is already active (i.e., the target is on
the path from root to the innermost active state, and the target is
not itself the innermost) now unwinds only the subtree strictly
below the target. The target's
exit:/entry:actions do NOT fire, and itsdefault(...)does NOT re-descend. You cannot re-enter a state you never left. The old semantics (exit target, re-enter, re-descend to default) was incorrect and could produce infinite re-entry when a child transitioned to its own parent. See spec §2.6, §T2.8, §T2.8b, §T2.8c. - Self-transitions (
target == innermost) are unchanged: the target is still exited and re-entered, timers still restart. - Lateral transitions (target not on the active path) are unchanged.
current_state()may now return a composite state when it became innermost via an up-transition.
0.1.0 — 2026-04-22
Initial release.
Added
statechart!proc macro: declarative Harel-style hierarchical state machines with nested states,entry:/exit:actions, LCA-aware transitions, default-child descent, andterminatesemantics.- Typed event payload bindings:
on(PacketRx { rssi: i16 }) => handler;expands into a typed handler signature, verified at compile time. - Timer triggers:
on(after Duration::from_secs(5)) => Next;andon(every Duration::from_millis(100)) => tick;, tracked in a fixed-capacityTimerTable. during:activities (v0.2 semantics): async functions that run while a state is on the active path and produce events. Multiple concurrent durings on a single state borrow disjoint&mutslices of the context, enforced by Rust's native split-borrow.- Runtime:
machine.run().awaitraces active durings, the external event channel, and timer deadlines — one future, cooperative on embassy, multi-threaded on tokio. - Feature gates:
tokio(std + tokio runtime),embassy(no_std + embassy-time / embassy-sync). The two features are mutually exclusive and enforced viacompile_error!. no_stdby default;stdpulled in only via thetokiofeature.STATE_CHARTconst anddispatch()helper for introspection.- Workspace:
hsmc(library) andhsmc-macros(proc-macro front-end) both at0.1.0.
Docs
- 320-line
README.mdwith quickstart, semantics, and examples. - Runnable examples:
microwaveandduring_radiounder tokio,embassy_fullunder embassy. - Behavior, hierarchy, during, timer, and payload-binding test suites (13 integration tests).