`default(...)` is now a transition with the same semantics as any
event-driven transition. It fires immediately after the declaring
state's entries finish and its durings start, applying the standard
exit-to-LCA / enter-to-target algorithm. Three behavioral changes
follow from this unification:
* Optional. A state with children is no longer required to declare a
`default(...)`. When omitted, the state itself is the resting
innermost active state; substates are reachable only via explicit
transitions. This is the "microwave Standby" pattern — Standby
has substates like LowPower that you enter only on demand, and
`=> Standby;` lands at Standby itself with no descent.
* Targets anywhere. The default target may be any state in the chart
— a child (the classic descent case), a sibling, an ancestor,
anywhere. `default(<sibling-or-ancestor>)` exits the just-entered
state as part of the default-fire transition; the same rule fires
whether you wrote it for descent, redirection, or up-bounce.
* Cycle-checked at compile time. The default-edge graph (out-degree
≤ 1 per node) is rejected if it contains any cycle, including
`default(self)`. `state A { default(A); }` and `A → B → A` chains
fail to compile with a pinpoint error message naming the cycle.
This guarantees default chains terminate at runtime.
Implementation:
* `validate.rs`: drop the "must be direct child" and "must have
children" restrictions; add `validate_default_graph` (iterative
three-color cycle detection via chain walk).
* `ir.rs`: keep the unresolved default ident through to
`resolve_transitions` so non-child targets resolve against the
global state-name map (the same map transition targets use).
* `codegen.rs`: precomputed `(from, to)` paths no longer fold default
descent into the enter slice — each path is LCA-relative
`(exit, enter)` ending exactly at the explicit target.
`descend_defaults` (a parent-pointer walk) is replaced by
`follow_defaults`, which fires real transitions through
`apply_path` and emits `TransitionFired{Internal}` /
`TransitionComplete` brackets per link. Up-transitions don't
trigger default-following (nothing was freshly entered, so no
state's default fires).
* `active_durings_per_innermost_state`: predicate widened to include
composites without a default (they can now be the innermost active
state, so any durings they declare must be raced when current).
BREAKING CHANGES:
* Journal format. Default-fires now emit `TransitionFired` (with
`reason: TransitionReason::Internal`) and `TransitionComplete`
brackets around their entry sequence. `Internal` was previously
reserved and never emitted; it is now active. Tests that
hand-build expected journal sequences must include these brackets
around every default-fire.
* Validation. `default(<non-child>)` and `default` on a childless
state previously raised compile-time errors. Both are now valid.
Code relying on these as guard rails must adapt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3234 lines
130 KiB
Rust
3234 lines
130 KiB
Rust
//! Code generation for the `statechart!` macro.
|
||
|
||
use proc_macro2::{Span, TokenStream};
|
||
use quote::{format_ident, quote};
|
||
use syn::Ident;
|
||
|
||
use crate::ir::{HandlerKindIr, Ir, StateIr, TriggerIr, VariantKind};
|
||
use crate::parse::{During, PayloadKind};
|
||
|
||
/// FNV-1a 64-bit hash of a chart's structural fingerprint. Stable across
|
||
/// rebuilds of the same chart definition; differs whenever any structural
|
||
/// element (state name, action name, transition target, default child,
|
||
/// timer trigger, event variant) changes.
|
||
///
|
||
/// Used as `<Chart>::CHART_HASH` so a journal recorded for one chart can
|
||
/// be rejected at replay time when the chart definition has drifted.
|
||
fn compute_chart_hash(ir: &Ir) -> u64 {
|
||
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
||
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
||
let mut h: u64 = FNV_OFFSET;
|
||
let mut feed = |bytes: &[u8]| {
|
||
for &b in bytes {
|
||
h ^= b as u64;
|
||
h = h.wrapping_mul(FNV_PRIME);
|
||
}
|
||
h ^= 0xff;
|
||
h = h.wrapping_mul(FNV_PRIME);
|
||
};
|
||
feed(ir.name.to_string().as_bytes());
|
||
for s in &ir.states {
|
||
feed(&s.id.to_le_bytes());
|
||
feed(s.name.to_string().as_bytes());
|
||
feed(&[
|
||
s.parent.unwrap_or(u16::MAX).to_le_bytes()[0],
|
||
s.parent.unwrap_or(u16::MAX).to_le_bytes()[1],
|
||
]);
|
||
feed(&s.depth.to_le_bytes());
|
||
feed(&s.default_child.unwrap_or(u16::MAX).to_le_bytes());
|
||
for &aid in &s.entries {
|
||
feed(&aid.to_le_bytes());
|
||
}
|
||
feed(&[0xee]); // separator: entries → exits
|
||
for &aid in &s.exits {
|
||
feed(&aid.to_le_bytes());
|
||
}
|
||
feed(&[0xff]); // separator: exits → handlers
|
||
for h_ir in &s.handlers {
|
||
match &h_ir.trigger {
|
||
TriggerIr::Event(id, ident, _) => {
|
||
feed(&[0x01]);
|
||
feed(&id.to_le_bytes());
|
||
feed(ident.to_string().as_bytes());
|
||
}
|
||
TriggerIr::Duration(id) => {
|
||
feed(&[0x02]);
|
||
feed(&id.to_le_bytes());
|
||
}
|
||
}
|
||
match &h_ir.kind {
|
||
HandlerKindIr::Transition(name, slot) => {
|
||
feed(&[0x10]);
|
||
feed(name.to_string().as_bytes());
|
||
feed(&slot.unwrap_or(u16::MAX).to_le_bytes());
|
||
}
|
||
HandlerKindIr::Action(aid, name) => {
|
||
feed(&[0x11]);
|
||
feed(&aid.to_le_bytes());
|
||
feed(name.to_string().as_bytes());
|
||
}
|
||
}
|
||
}
|
||
feed(&[0xfe]); // separator: handlers → timers
|
||
for &t in &s.owned_timers {
|
||
feed(&t.to_le_bytes());
|
||
}
|
||
feed(&[0xfd]); // separator: timers → durings
|
||
feed(&(s.durings.len() as u16).to_le_bytes());
|
||
}
|
||
feed(&[0xfc]); // separator: states → actions
|
||
for a in &ir.actions {
|
||
feed(a.name.to_string().as_bytes());
|
||
feed(&(a.params.len() as u16).to_le_bytes());
|
||
}
|
||
feed(&[0xfb]); // separator: actions → events
|
||
for e in &ir.event_variants {
|
||
feed(e.name.to_string().as_bytes());
|
||
}
|
||
h
|
||
}
|
||
|
||
/// (exit_data, enter_data, exit_range, enter_range) — flat blobs of u16
|
||
/// state ids and `(start, end)` index pairs into them, one per (from, to).
|
||
/// Returned by `compute_transition_paths()` and folded by the caller into
|
||
/// the consolidated `__PATH_DATA` / `__PATH_RANGE` tables.
|
||
type TransitionPathTables = (Vec<u16>, Vec<u16>, Vec<(u16, u16)>, Vec<(u16, u16)>);
|
||
|
||
/// Static precompute of every (from, to) state pair's exit and enter sequence,
|
||
/// folded into flat tables emitted alongside `__PARENT` / `__DEPTH`. Replaces
|
||
/// the runtime LCA depth-walk + ancestry walks + heapless::Vec construction
|
||
/// in `transition()` with two slice lookups and two straight slice iterations.
|
||
///
|
||
/// The (from, to) lookup matches the runtime LCA call: `from` is the active
|
||
/// innermost state (`self.current`), `to` is the transition target. The
|
||
/// same three transition shapes are encoded uniformly by what the
|
||
/// precomputed paths contain:
|
||
///
|
||
/// - **Up-transition** (`to` is a strict ancestor of `from`): exit_path is
|
||
/// `from..to` (target excluded; `to` is already on the active path and
|
||
/// must not be re-entered). enter_path is empty — `transition()` sets
|
||
/// `self.current = Some(target)` after the loops.
|
||
/// - **Self-transition** (`from == to`): exit_path is `[to]`. enter_path
|
||
/// is `[to]` — re-enter the target.
|
||
/// - **Lateral** (LCA is a proper ancestor of both): exit_path walks
|
||
/// `from` up to but not including LCA. enter_path walks
|
||
/// LCA-side child of `to` down to `to`.
|
||
///
|
||
/// Default-descent is **not** folded into these paths. After `transition()`
|
||
/// applies the precomputed exit/enter, it follows the default chain by
|
||
/// firing further transitions in a loop (`follow_defaults`). This is
|
||
/// because `default(...)` may target any state in the chart — sibling,
|
||
/// ancestor, anywhere — so each step of the chain is itself a full
|
||
/// LCA-aware transition, not a simple child descent. The compile-time
|
||
/// default-graph cycle check guarantees the loop terminates.
|
||
///
|
||
/// Storage: `__EXIT_DATA` and `__ENTER_DATA` are flat `[u16]` blobs;
|
||
/// `__EXIT_RANGE[i*N + j]` / `__ENTER_RANGE[i*N + j]` give `(start, end)`
|
||
/// offsets into them. Entries for unreachable pairs are still emitted (we
|
||
/// don't know which pairs are reachable without running the dispatcher's
|
||
/// full reachability analysis), but they cost only their few u16s of path
|
||
/// data and a single `(u16, u16)` index slot — and `transition()` never
|
||
/// looks them up at runtime.
|
||
fn compute_transition_paths(states: &[StateIr]) -> TransitionPathTables {
|
||
let n = states.len();
|
||
let parent = |s: u16| -> Option<u16> { states[s as usize].parent };
|
||
let depth = |s: u16| -> i32 { states[s as usize].depth as i32 };
|
||
|
||
let lca = |mut a: u16, mut b: u16| -> Option<u16> {
|
||
let mut da = depth(a);
|
||
let mut db = depth(b);
|
||
while da > db {
|
||
a = parent(a)?;
|
||
da -= 1;
|
||
}
|
||
while db > da {
|
||
b = parent(b)?;
|
||
db -= 1;
|
||
}
|
||
while a != b {
|
||
a = parent(a)?;
|
||
b = parent(b)?;
|
||
}
|
||
Some(a)
|
||
};
|
||
|
||
let is_strict_ancestor = |anc: u16, desc: u16| -> bool {
|
||
let mut cur = parent(desc);
|
||
while let Some(c) = cur {
|
||
if c == anc {
|
||
return true;
|
||
}
|
||
cur = parent(c);
|
||
}
|
||
false
|
||
};
|
||
|
||
let compute_one = |from: u16, to: u16| -> (Vec<u16>, Vec<u16>) {
|
||
// Up-transition: target is a strict ancestor of current. Exit
|
||
// strictly between them; do not re-enter target.
|
||
if from != to && is_strict_ancestor(to, from) {
|
||
let mut exit_path = Vec::new();
|
||
let mut cur = from;
|
||
while cur != to {
|
||
exit_path.push(cur);
|
||
cur = parent(cur).expect("up-transition walk must reach `to`");
|
||
}
|
||
return (exit_path, Vec::new());
|
||
}
|
||
|
||
// Self-transition: external semantics — exit and re-enter target.
|
||
// Default-following (if any) happens in the runtime chain loop.
|
||
if from == to {
|
||
return (vec![to], vec![to]);
|
||
}
|
||
|
||
// Lateral: exit `from` up to (not including) LCA; enter from
|
||
// LCA-child on `to`'s side down to `to`. Default-following
|
||
// happens in the runtime chain loop.
|
||
let lca_id = lca(from, to);
|
||
|
||
let mut exit_path = Vec::new();
|
||
let mut cur = from;
|
||
loop {
|
||
if Some(cur) == lca_id {
|
||
break;
|
||
}
|
||
exit_path.push(cur);
|
||
match parent(cur) {
|
||
Some(p) => cur = p,
|
||
None => break,
|
||
}
|
||
}
|
||
|
||
let mut rev = Vec::new();
|
||
let mut cur = to;
|
||
loop {
|
||
if Some(cur) == lca_id {
|
||
break;
|
||
}
|
||
rev.push(cur);
|
||
match parent(cur) {
|
||
Some(p) => cur = p,
|
||
None => break,
|
||
}
|
||
}
|
||
let enter_path: Vec<u16> = rev.into_iter().rev().collect();
|
||
|
||
(exit_path, enter_path)
|
||
};
|
||
|
||
let mut exit_data: Vec<u16> = Vec::new();
|
||
let mut enter_data: Vec<u16> = Vec::new();
|
||
let mut exit_range: Vec<(u16, u16)> = Vec::with_capacity(n * n);
|
||
let mut enter_range: Vec<(u16, u16)> = Vec::with_capacity(n * n);
|
||
|
||
for from in 0..n as u16 {
|
||
for to in 0..n as u16 {
|
||
let (ep, np) = compute_one(from, to);
|
||
|
||
let estart: u16 = exit_data
|
||
.len()
|
||
.try_into()
|
||
.expect("exit path data exceeds u16 offset range");
|
||
exit_data.extend(ep.iter().copied());
|
||
let eend: u16 = exit_data
|
||
.len()
|
||
.try_into()
|
||
.expect("exit path data exceeds u16 offset range");
|
||
exit_range.push((estart, eend));
|
||
|
||
let nstart: u16 = enter_data
|
||
.len()
|
||
.try_into()
|
||
.expect("enter path data exceeds u16 offset range");
|
||
enter_data.extend(np.iter().copied());
|
||
let nend: u16 = enter_data
|
||
.len()
|
||
.try_into()
|
||
.expect("enter path data exceeds u16 offset range");
|
||
enter_range.push((nstart, nend));
|
||
}
|
||
}
|
||
|
||
(exit_data, enter_data, exit_range, enter_range)
|
||
}
|
||
|
||
/// Static precompute of `do_terminate()`'s exit walk: for each state, the
|
||
/// chain `[current, parent(current), ..., __Root]` (root inclusive,
|
||
/// since `__Root` may carry root-level `entry:`/`exit:` actions declared
|
||
/// at the chart top level — see the `det_initial` test for that pattern).
|
||
///
|
||
/// Replaces the runtime `__PARENT` walk + per-iteration `match` in
|
||
/// `do_terminate` with one indexed slice into a flat blob. The terminate
|
||
/// path is always at most `H + 1` states long, so the total table size is
|
||
/// bounded by `N × (H_max + 1)` — under a KiB for any practical chart.
|
||
fn compute_terminate_paths(states: &[StateIr]) -> (Vec<u16>, Vec<(u16, u16)>) {
|
||
let n = states.len();
|
||
let parent = |s: u16| -> Option<u16> { states[s as usize].parent };
|
||
|
||
let mut data: Vec<u16> = Vec::new();
|
||
let mut range: Vec<(u16, u16)> = Vec::with_capacity(n);
|
||
for from in 0..n as u16 {
|
||
let start: u16 = data
|
||
.len()
|
||
.try_into()
|
||
.expect("terminate path data exceeds u16 offset range");
|
||
let mut cur = Some(from);
|
||
while let Some(s) = cur {
|
||
data.push(s);
|
||
cur = parent(s);
|
||
}
|
||
let end: u16 = data
|
||
.len()
|
||
.try_into()
|
||
.expect("terminate path data exceeds u16 offset range");
|
||
range.push((start, end));
|
||
}
|
||
(data, range)
|
||
}
|
||
|
||
/// Static precompute of the runtime event-bubble walk in `step()`.
|
||
///
|
||
/// Currently the dispatcher does:
|
||
///
|
||
/// ```text
|
||
/// while let Some(s) = __cur {
|
||
/// if let Some((actions, target)) = __handler_lookup(s, 0, __ev_id) { ... break; }
|
||
/// __cur = __PARENT[s as usize];
|
||
/// }
|
||
/// ```
|
||
///
|
||
/// — at runtime, walking up `__PARENT` until a state with an `on(EventX)`
|
||
/// handler is found. Each hop costs ~10-12 instructions (call + branch +
|
||
/// load), and the depth depends on where the handler is declared. With
|
||
/// every (state, event) pair statically resolvable from the chart, the
|
||
/// walk collapses to one indexed lookup.
|
||
///
|
||
/// For each `(current_state, event_id)` pair we materialize:
|
||
/// - `handler_state` — the deepest ancestor (or `current_state` itself)
|
||
/// with a matching `on()` clause, or `u16::MAX` if no ancestor handles
|
||
/// the event (runtime drops it).
|
||
/// - `action_ids` — the aggregated action ids that the original
|
||
/// `state_handler_arms` codegen would have produced for that
|
||
/// (state, event) pair, in declaration order.
|
||
/// - `target` — the transition target if any.
|
||
///
|
||
/// The aggregation rules mirror `state_handler_arms` exactly: per
|
||
/// (state, event), action handlers stack into an ordered Vec and the
|
||
/// (singular) transition target is captured if present. This is the
|
||
/// same shape `__handler_lookup` returns, just resolved at codegen time
|
||
/// for events. Timer triggers stay in `__handler_lookup` (the
|
||
/// `dispatch_trigger` path doesn't bubble).
|
||
fn compute_event_dispatch(ir: &Ir) -> Vec<(u16, Vec<u16>, Option<u16>)> {
|
||
use std::collections::BTreeMap;
|
||
|
||
/// `event_id → (action_ids, transition_target)` aggregation for one state.
|
||
type StateHandlers = BTreeMap<u16, (Vec<u16>, Option<u16>)>;
|
||
|
||
let n_states = ir.states.len();
|
||
let n_events = ir.event_variants.len();
|
||
|
||
// Per-state, per-event handler resolution. Mirrors the aggregation in
|
||
// `state_handler_arms` (sort by decl_index, group by trigger).
|
||
let mut per_state: Vec<StateHandlers> = vec![StateHandlers::new(); n_states];
|
||
for (sid, state) in ir.states.iter().enumerate() {
|
||
let mut handlers = state.handlers.iter().collect::<Vec<_>>();
|
||
handlers.sort_by_key(|h| h.decl_index);
|
||
for h in handlers {
|
||
let event_id = match &h.trigger {
|
||
TriggerIr::Event(id, _, _) => *id,
|
||
TriggerIr::Duration(_) => continue,
|
||
};
|
||
let entry = per_state[sid].entry(event_id).or_default();
|
||
match &h.kind {
|
||
HandlerKindIr::Action(aid, _) => entry.0.push(*aid),
|
||
HandlerKindIr::Transition(_, Some(tid)) => entry.1 = Some(*tid),
|
||
HandlerKindIr::Transition(_, None) => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// For each (state, event_id), bubble up __PARENT and stop at the
|
||
// first state with a matching handler. No-handler ⇒ sentinel.
|
||
let mut out = Vec::with_capacity(n_states * n_events);
|
||
for state in 0..n_states {
|
||
for event_id in 0..n_events as u16 {
|
||
let mut cur = Some(state as u16);
|
||
let mut resolved: Option<(u16, Vec<u16>, Option<u16>)> = None;
|
||
while let Some(s) = cur {
|
||
if let Some((actions, target)) = per_state[s as usize].get(&event_id) {
|
||
resolved = Some((s, actions.clone(), *target));
|
||
break;
|
||
}
|
||
cur = ir.states[s as usize].parent;
|
||
}
|
||
out.push(resolved.unwrap_or((u16::MAX, Vec::new(), None)));
|
||
}
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
/// For each state that can be the innermost active state, return its state id
|
||
/// and the flattened list of `during:` activities active there (declared on it
|
||
/// or on any ancestor) in leaf-first order. Durings declared on the same state
|
||
/// follow their declaration order.
|
||
///
|
||
/// A state can be innermost iff it has no children, OR it has children but no
|
||
/// `default` child (so default-descent stops at it). Composites with a default
|
||
/// child are always descended past at runtime, so they're skipped here.
|
||
///
|
||
/// Innermost-able states with no active durings are omitted — the run loop
|
||
/// uses its default (channel+timer) path for those.
|
||
fn active_durings_per_innermost_state(ir: &Ir) -> Vec<(u16, Vec<During>)> {
|
||
let mut out = Vec::new();
|
||
for s in &ir.states {
|
||
if !s.children.is_empty() && s.default_child.is_some() {
|
||
continue;
|
||
}
|
||
if s.parent.is_none() {
|
||
continue; // synthetic __Root
|
||
}
|
||
let mut chain: Vec<During> = Vec::new();
|
||
let mut cur = Some(s.id);
|
||
while let Some(id) = cur {
|
||
for d in &ir.states[id as usize].durings {
|
||
chain.push(d.clone());
|
||
}
|
||
cur = ir.states[id as usize].parent;
|
||
}
|
||
if !chain.is_empty() {
|
||
out.push((s.id, chain));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Emit `let dN = fn(&mut self.ctx.fN_0, &mut self.ctx.fN_1, ...);` for each
|
||
/// during in `chain`. Returns the list of binding identifiers in order so the
|
||
/// call site can reference them in the `select!`/`select_N` arguments.
|
||
fn emit_during_bindings(chain: &[During]) -> (Vec<Ident>, TokenStream) {
|
||
let mut idents = Vec::with_capacity(chain.len());
|
||
let mut stmts: Vec<TokenStream> = Vec::with_capacity(chain.len());
|
||
for (i, d) in chain.iter().enumerate() {
|
||
let bind = format_ident!("__hsmc_d{}", i);
|
||
let fn_name = &d.fn_name;
|
||
let field_refs = d.fields.iter().map(|f| quote! { &mut self.ctx.#f });
|
||
stmts.push(quote! {
|
||
let #bind = #fn_name(#(#field_refs),*);
|
||
});
|
||
idents.push(bind);
|
||
}
|
||
(idents, quote! { #(#stmts)* })
|
||
}
|
||
|
||
/// Emit the per-state match arms for the tokio run loop, keyed on the
|
||
/// innermost active state id. Each arm constructs the active during: futures
|
||
/// (split-borrowing context fields), then races them against the event channel
|
||
/// and the next-timer sleep via `tokio::select!`. The default arm — used for
|
||
/// states with no active durings — is emitted by the caller.
|
||
fn emit_tokio_race_arms_with_channel(ir: &Ir) -> Vec<TokenStream> {
|
||
let mut arms = Vec::new();
|
||
for (sid, chain) in active_durings_per_innermost_state(ir) {
|
||
let (binds, stmts) = emit_during_bindings(&chain);
|
||
let select_arms = binds.iter().map(|b| {
|
||
quote! { ev = #b => __HsmcRace::Event(ev), }
|
||
});
|
||
arms.push(quote! {
|
||
Some(#sid) => {
|
||
#stmts
|
||
let sleep = ::tokio::time::sleep(sleep_dur);
|
||
::tokio::pin!(sleep);
|
||
let rx = self.__tokio_rx.as_mut().expect("hsmc: tokio channel");
|
||
::tokio::select! {
|
||
biased;
|
||
#(#select_arms)*
|
||
maybe = rx.recv() => match maybe {
|
||
Some(ev) => __HsmcRace::Event(ev),
|
||
None => __HsmcRace::ChannelClosed,
|
||
},
|
||
_ = &mut sleep => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
});
|
||
}
|
||
arms
|
||
}
|
||
|
||
/// Same as `emit_tokio_race_arms_with_channel` but for timer-only machines
|
||
/// (no `events:` declared, no channel). The default arm races only the timer.
|
||
fn emit_tokio_race_arms_timer_only(ir: &Ir) -> Vec<TokenStream> {
|
||
let mut arms = Vec::new();
|
||
for (sid, chain) in active_durings_per_innermost_state(ir) {
|
||
let (binds, stmts) = emit_during_bindings(&chain);
|
||
let select_arms = binds.iter().map(|b| {
|
||
quote! { ev = #b => __HsmcRace::Event(ev), }
|
||
});
|
||
arms.push(quote! {
|
||
Some(#sid) => {
|
||
#stmts
|
||
let sleep = ::tokio::time::sleep(sleep_dur);
|
||
::tokio::pin!(sleep);
|
||
::tokio::select! {
|
||
biased;
|
||
#(#select_arms)*
|
||
_ = &mut sleep => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
});
|
||
}
|
||
arms
|
||
}
|
||
|
||
/// Build a human-readable ASCII tree of the machine's hierarchy, suitable
|
||
/// for emitting as a `STATE_CHART` const. Each state shows its default-child
|
||
/// marker, entry/exit actions (if any), during activities, and handler
|
||
/// triggers. Used for `defmt::info!(M::STATE_CHART)` or panic messages.
|
||
fn emit_state_chart_string(ir: &Ir) -> String {
|
||
let mut out = String::new();
|
||
// Start with the machine name as the root label (root state name is
|
||
// synthetic `__Root` — don't surface it).
|
||
out.push_str(&ir.name.to_string());
|
||
out.push('\n');
|
||
// Top-level children of __Root: walk via __Root's children list.
|
||
let root_children: Vec<u16> = ir.states[0].children.clone();
|
||
let default_child_of_root = ir.states[0].default_child;
|
||
for (i, cid) in root_children.iter().enumerate() {
|
||
let last = i + 1 == root_children.len();
|
||
let is_default = Some(*cid) == default_child_of_root;
|
||
render_state(ir, *cid, "", last, is_default, &mut out);
|
||
}
|
||
out
|
||
}
|
||
|
||
fn render_state(
|
||
ir: &Ir,
|
||
sid: u16,
|
||
prefix: &str,
|
||
is_last: bool,
|
||
is_default: bool,
|
||
out: &mut String,
|
||
) {
|
||
let s = &ir.states[sid as usize];
|
||
let branch = if is_last { "└── " } else { "├── " };
|
||
out.push_str(prefix);
|
||
out.push_str(branch);
|
||
if is_default {
|
||
out.push_str("[default] ");
|
||
}
|
||
out.push_str(&s.name.to_string());
|
||
out.push('\n');
|
||
let child_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
|
||
// Entry actions
|
||
for aid in &s.entries {
|
||
let name = &ir.actions[*aid as usize].name;
|
||
out.push_str(&child_prefix);
|
||
out.push_str("• entry: ");
|
||
out.push_str(&name.to_string());
|
||
out.push('\n');
|
||
}
|
||
// Exit actions
|
||
for aid in &s.exits {
|
||
let name = &ir.actions[*aid as usize].name;
|
||
out.push_str(&child_prefix);
|
||
out.push_str("• exit: ");
|
||
out.push_str(&name.to_string());
|
||
out.push('\n');
|
||
}
|
||
// Durings
|
||
for d in &s.durings {
|
||
out.push_str(&child_prefix);
|
||
out.push_str("• during: ");
|
||
out.push_str(&d.fn_name.to_string());
|
||
out.push('(');
|
||
for (i, f) in d.fields.iter().enumerate() {
|
||
if i > 0 {
|
||
out.push_str(", ");
|
||
}
|
||
out.push_str(&f.to_string());
|
||
}
|
||
out.push_str(")\n");
|
||
}
|
||
// Handlers (summary)
|
||
for h in &s.handlers {
|
||
let trig = match &h.trigger {
|
||
crate::ir::TriggerIr::Event(_, name, _) => format!("on({})", name),
|
||
crate::ir::TriggerIr::Duration(tid) => {
|
||
let d = &ir.duration_triggers[*tid as usize];
|
||
let verb = if d.repeat { "every" } else { "after" };
|
||
format!("on({} {})", verb, d.key.split(':').next().unwrap_or("?"))
|
||
}
|
||
};
|
||
let target = match &h.kind {
|
||
crate::ir::HandlerKindIr::Transition(tgt, _) => format!("→ {}", tgt),
|
||
crate::ir::HandlerKindIr::Action(_, name) => format!("⇒ {}", name),
|
||
};
|
||
out.push_str(&child_prefix);
|
||
out.push_str("• ");
|
||
out.push_str(&trig);
|
||
out.push(' ');
|
||
out.push_str(&target);
|
||
out.push('\n');
|
||
}
|
||
// Child states
|
||
let default_child_of_s = s.default_child;
|
||
for (i, cid) in s.children.iter().enumerate() {
|
||
let last = i + 1 == s.children.len();
|
||
let is_def = Some(*cid) == default_child_of_s;
|
||
render_state(ir, *cid, &child_prefix, last, is_def, out);
|
||
}
|
||
}
|
||
|
||
/// Return (select_fn_ident, either_variant_idents) for embassy_futures::select
|
||
/// with the given total arity. `arity` must be one of 2, 3, 4, 5, 6.
|
||
fn embassy_select_for_arity(arity: usize) -> (Ident, Vec<Ident>) {
|
||
match arity {
|
||
2 => (
|
||
format_ident!("select"),
|
||
vec![format_ident!("First"), format_ident!("Second")],
|
||
),
|
||
3 => (
|
||
format_ident!("select3"),
|
||
vec![
|
||
format_ident!("First"),
|
||
format_ident!("Second"),
|
||
format_ident!("Third"),
|
||
],
|
||
),
|
||
4 => (
|
||
format_ident!("select4"),
|
||
vec![
|
||
format_ident!("First"),
|
||
format_ident!("Second"),
|
||
format_ident!("Third"),
|
||
format_ident!("Fourth"),
|
||
],
|
||
),
|
||
5 => (
|
||
format_ident!("select5"),
|
||
vec![
|
||
format_ident!("First"),
|
||
format_ident!("Second"),
|
||
format_ident!("Third"),
|
||
format_ident!("Fourth"),
|
||
format_ident!("Fifth"),
|
||
],
|
||
),
|
||
6 => (
|
||
format_ident!("select6"),
|
||
vec![
|
||
format_ident!("First"),
|
||
format_ident!("Second"),
|
||
format_ident!("Third"),
|
||
format_ident!("Fourth"),
|
||
format_ident!("Fifth"),
|
||
format_ident!("Sixth"),
|
||
],
|
||
),
|
||
_ => unreachable!("embassy_select_for_arity called with unsupported arity"),
|
||
}
|
||
}
|
||
|
||
/// Emit embassy run-loop match arms for machines with a bound channel. Arity
|
||
/// per state = active_durings + 2 (channel + timer). Embassy caps at `select6`
|
||
/// → max 4 concurrent durings on a path with events. Over-limit paths emit
|
||
/// a `compile_error!` inside the embassy-only arm so tokio codegen still
|
||
/// compiles unchanged.
|
||
fn emit_embassy_race_arms_with_channel(ir: &Ir) -> Vec<TokenStream> {
|
||
let mut arms = Vec::new();
|
||
for (sid, chain) in active_durings_per_innermost_state(ir) {
|
||
let n = chain.len();
|
||
if n + 2 > 6 {
|
||
let msg = format!(
|
||
"too many concurrent `during:` activities on the active path for state id {} \
|
||
under the embassy feature ({} + channel + timer > 6); embassy_futures supports \
|
||
at most 4 concurrent durings on a path with events. Combine activities into a \
|
||
single during that uses `select` internally.",
|
||
sid, n
|
||
);
|
||
arms.push(quote! {
|
||
Some(#sid) => { ::core::compile_error!(#msg); unreachable!() }
|
||
});
|
||
continue;
|
||
}
|
||
let (binds, stmts) = emit_during_bindings(&chain);
|
||
let (sel_fn, variants) = embassy_select_for_arity(n + 2);
|
||
let ch_variant = &variants[n];
|
||
let tm_variant = &variants[n + 1];
|
||
let call_args = binds
|
||
.iter()
|
||
.map(|b| quote! { #b })
|
||
.chain(std::iter::once(quote! { rx.receive() }))
|
||
.chain(std::iter::once(quote! { timer_fut }));
|
||
let either_path = if n + 2 == 2 {
|
||
quote! { ::embassy_futures::select::Either }
|
||
} else {
|
||
let either_ty = format_ident!("Either{}", (n + 2).to_string());
|
||
quote! { ::embassy_futures::select::#either_ty }
|
||
};
|
||
let during_match = binds.iter().enumerate().map(|(i, _b)| {
|
||
let v = &variants[i];
|
||
quote! { #either_path::#v(ev) => __HsmcRace::Event(ev), }
|
||
});
|
||
arms.push(quote! {
|
||
Some(#sid) => {
|
||
#stmts
|
||
let timer_fut = async {
|
||
match next {
|
||
Some(d) => {
|
||
::embassy_time::Timer::after(
|
||
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
||
).await;
|
||
}
|
||
None => ::core::future::pending::<()>().await,
|
||
}
|
||
};
|
||
let rx = self.__embassy_rx.as_ref().expect("hsmc: embassy channel");
|
||
match ::embassy_futures::select::#sel_fn(#(#call_args),*).await {
|
||
#(#during_match)*
|
||
#either_path::#ch_variant(ev) => __HsmcRace::Event(ev),
|
||
#either_path::#tm_variant(()) => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
});
|
||
}
|
||
arms
|
||
}
|
||
|
||
/// Emit embassy run-loop match arms for timer-only machines (no channel).
|
||
/// Arity per state = active_durings + 1 (timer only). Max 5 concurrent
|
||
/// durings on a timer-only path.
|
||
fn emit_embassy_race_arms_timer_only(ir: &Ir) -> Vec<TokenStream> {
|
||
let mut arms = Vec::new();
|
||
for (sid, chain) in active_durings_per_innermost_state(ir) {
|
||
let n = chain.len();
|
||
if n == 0 {
|
||
continue; // falls through to default (just timer)
|
||
}
|
||
if n + 1 > 6 {
|
||
let msg = format!(
|
||
"too many concurrent `during:` activities on the active path for state id {} \
|
||
under the embassy feature ({} + timer > 6); embassy_futures supports at most 5 \
|
||
concurrent durings on a timer-only path.",
|
||
sid, n
|
||
);
|
||
arms.push(quote! {
|
||
Some(#sid) => { ::core::compile_error!(#msg); unreachable!() }
|
||
});
|
||
continue;
|
||
}
|
||
let (binds, stmts) = emit_during_bindings(&chain);
|
||
let (sel_fn, variants) = embassy_select_for_arity(n + 1);
|
||
let call_args = binds
|
||
.iter()
|
||
.map(|b| quote! { #b })
|
||
.chain(std::iter::once(quote! { timer_fut }));
|
||
let either_path = if n + 1 == 2 {
|
||
quote! { ::embassy_futures::select::Either }
|
||
} else {
|
||
let either_ty = format_ident!("Either{}", (n + 1).to_string());
|
||
quote! { ::embassy_futures::select::#either_ty }
|
||
};
|
||
let tm_variant = &variants[n];
|
||
let during_match = binds.iter().enumerate().map(|(i, _b)| {
|
||
let v = &variants[i];
|
||
quote! { #either_path::#v(ev) => __HsmcRace::Event(ev), }
|
||
});
|
||
arms.push(quote! {
|
||
Some(#sid) => {
|
||
#stmts
|
||
let timer_fut = async {
|
||
match next {
|
||
Some(d) => {
|
||
::embassy_time::Timer::after(
|
||
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
||
).await;
|
||
}
|
||
None => ::core::future::pending::<()>().await,
|
||
}
|
||
};
|
||
match ::embassy_futures::select::#sel_fn(#(#call_args),*).await {
|
||
#(#during_match)*
|
||
#either_path::#tm_variant(()) => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
});
|
||
}
|
||
arms
|
||
}
|
||
|
||
pub fn generate(ir: &Ir) -> TokenStream {
|
||
let machine_name = &ir.name;
|
||
let state_enum_name = format_ident!("{}State", machine_name);
|
||
let actions_trait_name = format_ident!("{}Actions", machine_name);
|
||
let action_ctx_name = format_ident!("{}ActionContext", machine_name);
|
||
let ctx_alias_name = format_ident!("{}Ctx", machine_name);
|
||
let sender_name = format_ident!("{}Sender", machine_name);
|
||
let ctx_ty = &ir.context_ty;
|
||
let ev_ty = &ir.event_ty;
|
||
|
||
let n_states = ir.states.len();
|
||
let n_actions = ir.actions.len();
|
||
let n_dur_triggers = ir.duration_triggers.len();
|
||
let max_timers = n_dur_triggers.max(1);
|
||
|
||
let chart_hash = compute_chart_hash(ir);
|
||
let chart_hash_lit = quote! { #chart_hash };
|
||
|
||
// Public state enum — only user-declared states (skip __Root).
|
||
let state_variants: Vec<_> = ir
|
||
.states
|
||
.iter()
|
||
.filter(|s| s.parent.is_some())
|
||
.map(|s| s.name.clone())
|
||
.collect();
|
||
|
||
let entries_of_arms: Vec<_> = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let sid = s.id;
|
||
let entries_tok: Vec<_> = s.entries.iter().map(|a| quote! { #a }).collect();
|
||
quote! { #sid => &[#(#entries_tok),*], }
|
||
})
|
||
.collect();
|
||
let exits_of_arms: Vec<_> = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let sid = s.id;
|
||
let exits_tok: Vec<_> = s.exits.iter().map(|a| quote! { #a }).collect();
|
||
quote! { #sid => &[#(#exits_tok),*], }
|
||
})
|
||
.collect();
|
||
|
||
// ── Per-id name lookup tables ─────────────────────────────────────
|
||
//
|
||
// Generated unconditionally at chart scope so any code in the
|
||
// emitted module (machine impl, action context, etc.) can resolve
|
||
// a u16 id to a `&'static str`. The tables are dead-code-eliminated
|
||
// when no observation feature is on: with neither `journal` nor any
|
||
// `trace-*` feature enabled, the `__chart_observe!` macro arms are
|
||
// empty, so the call sites never reference these helpers and the
|
||
// linker drops them. Marked `dead_code` to silence rustc when off.
|
||
let chart_name_lit = ir.name.to_string();
|
||
|
||
let state_name_arms: Vec<_> = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let sid = s.id;
|
||
let nm = if s.parent.is_none() {
|
||
// Synthesized root: render as the chart name (per spec
|
||
// the root state IS the chart).
|
||
ir.name.to_string()
|
||
} else {
|
||
s.name.to_string()
|
||
};
|
||
quote! { #sid => #nm, }
|
||
})
|
||
.collect();
|
||
let action_name_arms: Vec<_> = ir
|
||
.actions
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, a)| {
|
||
let aid = i as u16;
|
||
let nm = a.name.to_string();
|
||
quote! { #aid => #nm, }
|
||
})
|
||
.collect();
|
||
let event_name_arms: Vec<_> = ir
|
||
.event_variants
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| {
|
||
let eid = i as u16;
|
||
let nm = v.name.to_string();
|
||
quote! { #eid => #nm, }
|
||
})
|
||
.collect();
|
||
let timer_name_arms: Vec<_> = ir
|
||
.duration_triggers
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, d)| {
|
||
let tid = i as u16;
|
||
// The intern key looks like "<state>::<expr>:<repeat>". For
|
||
// human readability render the expr text — fall back to
|
||
// numeric id if the key parses oddly.
|
||
let label = format!("t{}", tid);
|
||
// Try to extract the expression body so the trace shows
|
||
// something like `Duration::from_secs(5)`. Using `key` as
|
||
// a stable label is OK for now; the expr token stream isn't
|
||
// round-tripped through `to_string()` cleanly across rustc
|
||
// versions, so prefer a synthesized short name.
|
||
let _ = &d.key; // mark used for non-warn
|
||
quote! { #tid => #label, }
|
||
})
|
||
.collect();
|
||
// Per-(state, during-index) name lookup. The during's fn-name is
|
||
// the closest thing to a stable identifier the user wrote.
|
||
let during_name_arms: Vec<_> = {
|
||
let mut arms = Vec::new();
|
||
for s in &ir.states {
|
||
let sid = s.id;
|
||
for (di, d) in s.durings.iter().enumerate() {
|
||
let did = di as u16;
|
||
let nm = d.fn_name.to_string();
|
||
arms.push(quote! { (#sid, #did) => #nm, });
|
||
}
|
||
}
|
||
arms
|
||
};
|
||
// Pre-rendered transition reason strings for trace output.
|
||
// `event:<VariantName>` for event-driven transitions.
|
||
let event_reason_str_arms: Vec<_> = ir
|
||
.event_variants
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| {
|
||
let eid = i as u16;
|
||
let s = format!("event:{}", v.name);
|
||
quote! { #eid => #s, }
|
||
})
|
||
.collect();
|
||
// `timer:<state>/<timer>` for timer-driven transitions. We
|
||
// enumerate (state, timer) pairs that actually occur.
|
||
let timer_reason_str_arms: Vec<_> = {
|
||
let mut arms = Vec::new();
|
||
for s in &ir.states {
|
||
for &tid in &s.owned_timers {
|
||
let sid = s.id;
|
||
let sname = if s.parent.is_none() {
|
||
ir.name.to_string()
|
||
} else {
|
||
s.name.to_string()
|
||
};
|
||
let tlabel = format!("t{}", tid);
|
||
let s_str = format!("timer:{}/{}", sname, tlabel);
|
||
arms.push(quote! { (#sid, #tid) => #s_str, });
|
||
}
|
||
}
|
||
arms
|
||
};
|
||
|
||
let name_helpers = quote! {
|
||
#[allow(dead_code)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __state_name(sid: u16) -> &'static str {
|
||
match sid {
|
||
#(#state_name_arms)*
|
||
_ => "<unknown>",
|
||
}
|
||
}
|
||
#[allow(dead_code)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __action_name(aid: u16) -> &'static str {
|
||
match aid {
|
||
#(#action_name_arms)*
|
||
_ => "<unknown>",
|
||
}
|
||
}
|
||
#[allow(dead_code, unreachable_patterns)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __event_name(eid: u16) -> &'static str {
|
||
match eid {
|
||
#(#event_name_arms)*
|
||
_ => "<unknown>",
|
||
}
|
||
}
|
||
#[allow(dead_code, unreachable_patterns)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __timer_name(tid: u16) -> &'static str {
|
||
match tid {
|
||
#(#timer_name_arms)*
|
||
_ => "<unknown>",
|
||
}
|
||
}
|
||
#[allow(dead_code, unreachable_patterns, unused_variables)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __during_name(sid: u16, did: u16) -> &'static str {
|
||
match (sid, did) {
|
||
#(#during_name_arms)*
|
||
_ => "<unknown>",
|
||
}
|
||
}
|
||
#[allow(dead_code, unreachable_patterns)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __event_reason_str(eid: u16) -> &'static str {
|
||
match eid {
|
||
#(#event_reason_str_arms)*
|
||
_ => "event:?",
|
||
}
|
||
}
|
||
#[allow(dead_code, unreachable_patterns, unused_variables)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __timer_reason_str(sid: u16, tid: u16) -> &'static str {
|
||
match (sid, tid) {
|
||
#(#timer_reason_str_arms)*
|
||
_ => "timer:?",
|
||
}
|
||
}
|
||
#[allow(dead_code)]
|
||
#[doc(hidden)]
|
||
#[inline]
|
||
fn __from_name(opt: Option<u16>) -> &'static str {
|
||
match opt {
|
||
Some(id) => __state_name(id),
|
||
None => "<none>",
|
||
}
|
||
}
|
||
};
|
||
|
||
// ── Observation hook snippets ─────────────────────────────────────
|
||
//
|
||
// Each snippet emits ONE `__chart_observe!` invocation. The macro
|
||
// dispatches per-variant to whichever sinks (journal + trace-*) are
|
||
// enabled at compile time. Trace and journal share the same call
|
||
// sites by construction — they cannot diverge.
|
||
//
|
||
// Names (state/action/event/timer/during) are passed as runtime
|
||
// lookups against the small static tables generated above. With no
|
||
// sink feature on, the macro arm body is empty and the lookup is
|
||
// never expanded — zero overhead at the call site.
|
||
|
||
let observe_started = quote! {
|
||
::hsmc::__chart_observe!(
|
||
Started,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
Self::CHART_HASH
|
||
);
|
||
};
|
||
let observe_enter_began = quote! {
|
||
::hsmc::__chart_observe!(
|
||
EnterBegan,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid,
|
||
__state_name(sid)
|
||
);
|
||
};
|
||
let observe_entered = quote! {
|
||
::hsmc::__chart_observe!(
|
||
Entered,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid,
|
||
__state_name(sid)
|
||
);
|
||
};
|
||
let observe_exit_began = quote! {
|
||
::hsmc::__chart_observe!(
|
||
ExitBegan,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid,
|
||
__state_name(sid)
|
||
);
|
||
};
|
||
let observe_exited = quote! {
|
||
::hsmc::__chart_observe!(
|
||
Exited,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid,
|
||
__state_name(sid)
|
||
);
|
||
};
|
||
let observe_action_entry = quote! {
|
||
::hsmc::__chart_observe!(
|
||
ActionInvoked,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
aid, __action_name(aid),
|
||
Entry
|
||
);
|
||
};
|
||
let observe_action_exit = quote! {
|
||
::hsmc::__chart_observe!(
|
||
ActionInvoked,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
aid, __action_name(aid),
|
||
Exit
|
||
);
|
||
};
|
||
let observe_action_handler = quote! {
|
||
::hsmc::__chart_observe!(
|
||
ActionInvoked,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__handler_state, __state_name(__handler_state),
|
||
aid, __action_name(aid),
|
||
Handler
|
||
);
|
||
};
|
||
// TransitionFired — three call-site flavours. Codegen splices in
|
||
// the appropriate one based on what triggered the transition.
|
||
let observe_transition_fired_event = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TransitionFired,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
self.current, __from_name(self.current),
|
||
target, __state_name(target),
|
||
::hsmc::TransitionReason::Event { event: __ev_id },
|
||
__event_reason_str(__ev_id)
|
||
);
|
||
};
|
||
let observe_transition_fired_timer = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TransitionFired,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
self.current, __from_name(self.current),
|
||
target, __state_name(target),
|
||
::hsmc::TransitionReason::Timer {
|
||
state: __t_state,
|
||
timer: __t_trigger,
|
||
},
|
||
__timer_reason_str(__t_state, __t_trigger)
|
||
);
|
||
};
|
||
let observe_transition_fired_internal = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TransitionFired,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
self.current, __from_name(self.current),
|
||
target, __state_name(target),
|
||
::hsmc::TransitionReason::Internal,
|
||
"internal"
|
||
);
|
||
};
|
||
let observe_transition_complete = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TransitionComplete,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__t_complete_from, __from_name(__t_complete_from),
|
||
target, __state_name(target)
|
||
);
|
||
};
|
||
let observe_event_received = quote! {
|
||
::hsmc::__chart_observe!(
|
||
EventReceived,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id,
|
||
__event_name(__ev_id)
|
||
);
|
||
};
|
||
let observe_event_delivered = quote! {
|
||
::hsmc::__chart_observe!(
|
||
EventDelivered,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id, __event_name(__ev_id),
|
||
__handler_state, __state_name(__handler_state)
|
||
);
|
||
};
|
||
let observe_event_dropped = quote! {
|
||
::hsmc::__chart_observe!(
|
||
EventDropped,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id, __event_name(__ev_id)
|
||
);
|
||
};
|
||
let observe_timer_armed = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TimerArmed,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
tid, __timer_name(tid),
|
||
__duration_for(tid).as_nanos() as u64
|
||
);
|
||
};
|
||
let observe_timer_cancelled = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TimerCancelled,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
tid, __timer_name(tid)
|
||
);
|
||
};
|
||
let observe_timer_fired = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TimerFired,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__t_state, __state_name(__t_state),
|
||
__t_trigger, __timer_name(__t_trigger)
|
||
);
|
||
};
|
||
let observe_terminated = quote! {
|
||
::hsmc::__chart_observe!(
|
||
Terminated,
|
||
&mut self.__journal,
|
||
#chart_name_lit
|
||
);
|
||
};
|
||
let observe_terminate_requested = quote! {
|
||
::hsmc::__chart_observe!(
|
||
TerminateRequested,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id, __event_name(__ev_id)
|
||
);
|
||
};
|
||
|
||
// Codegen-static elide of the timer-table machinery for charts that
|
||
// declare no `Duration` triggers. The TimerTable is still constructed
|
||
// (the field is part of the machine struct so the generic parameters
|
||
// line up), but every per-step / per-exit call into it is gone — no
|
||
// empty-table branch, no `min_remaining()`-returns-None call, no
|
||
// `cancel_state()`. `step()` returns `None` directly for the
|
||
// next-tick deadline. For the bench charts (none of which arm
|
||
// timers) this drops several instructions per `step()` and one per
|
||
// `exit_state()`. `dispatch_trigger()` and the timer-fired arms
|
||
// remain in the binary but are unreachable; LLVM DCEs them.
|
||
let has_timers = !ir.duration_triggers.is_empty();
|
||
let timer_decrement_call = if has_timers {
|
||
quote! { self.timers.decrement(elapsed); }
|
||
} else {
|
||
quote! {}
|
||
};
|
||
let timer_pop_block_sync = if has_timers {
|
||
quote! {
|
||
if let Some((state, trig)) = self.timers.pop_expired(&__DEPTH) {
|
||
let __t_state = state;
|
||
let __t_trigger = trig;
|
||
#observe_timer_fired
|
||
if __duration_repeats(trig) {
|
||
let d = __duration_for(trig);
|
||
self.timers.start(state, trig, d);
|
||
let sid = state;
|
||
let tid = trig;
|
||
#observe_timer_armed
|
||
}
|
||
self.dispatch_trigger(state, 1, trig);
|
||
return self.timers.min_remaining();
|
||
}
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
let timer_pop_block_async = if has_timers {
|
||
quote! {
|
||
if let Some((state, trig)) = self.timers.pop_expired(&__DEPTH) {
|
||
let __t_state = state;
|
||
let __t_trigger = trig;
|
||
#observe_timer_fired
|
||
if __duration_repeats(trig) {
|
||
let d = __duration_for(trig);
|
||
self.timers.start(state, trig, d);
|
||
let sid = state;
|
||
let tid = trig;
|
||
#observe_timer_armed
|
||
}
|
||
self.dispatch_trigger(state, 1, trig).await;
|
||
return self.timers.min_remaining();
|
||
}
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
let timer_min_remaining_expr = if has_timers {
|
||
quote! { self.timers.min_remaining() }
|
||
} else {
|
||
quote! { None }
|
||
};
|
||
let timer_cancel_state_call = if has_timers {
|
||
quote! { self.timers.cancel_state(sid); }
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
// Per-state during count, used to emit DuringStarted/Cancelled
|
||
// events one per declared during in the state.
|
||
let durings_count_arms: Vec<TokenStream> = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let sid = s.id;
|
||
let n = s.durings.len() as u16;
|
||
quote! { #sid => #n, }
|
||
})
|
||
.collect();
|
||
let observe_durings_started = quote! {
|
||
let __dn = Self::__durings_count(sid);
|
||
let mut __di: u16 = 0;
|
||
while __di < __dn {
|
||
::hsmc::__chart_observe!(
|
||
DuringStarted,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
__di, __during_name(sid, __di)
|
||
);
|
||
__di += 1;
|
||
}
|
||
};
|
||
let observe_durings_cancelled = quote! {
|
||
let __dn = Self::__durings_count(sid);
|
||
let mut __di: u16 = 0;
|
||
while __di < __dn {
|
||
::hsmc::__chart_observe!(
|
||
DuringCancelled,
|
||
&mut self.__journal,
|
||
#chart_name_lit,
|
||
sid, __state_name(sid),
|
||
__di, __during_name(sid, __di)
|
||
);
|
||
__di += 1;
|
||
}
|
||
};
|
||
|
||
let parent_table = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| match s.parent {
|
||
Some(p) => quote! { Some(#p) },
|
||
None => quote! { None },
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let depth_table = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let d = s.depth;
|
||
quote! { #d }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let default_child_table = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| match s.default_child {
|
||
Some(c) => quote! { Some(#c) },
|
||
None => quote! { None },
|
||
})
|
||
.collect::<Vec<_>>();
|
||
// Path data is concatenated [exit; enter] per pair into a single
|
||
// `__PATH_DATA` blob; one packed `(start, mid, end)` per pair indexes
|
||
// both halves. One range lookup, one base array — minimizes cold-cache
|
||
// line touches on the dispatch hot path (vs four separate tables).
|
||
let (
|
||
transition_exit_data,
|
||
transition_enter_data,
|
||
transition_exit_range,
|
||
transition_enter_range,
|
||
) = compute_transition_paths(&ir.states);
|
||
let mut path_data: Vec<u16> =
|
||
Vec::with_capacity(transition_exit_data.len() + transition_enter_data.len());
|
||
let mut path_range: Vec<(u16, u16, u16)> = Vec::with_capacity(transition_exit_range.len());
|
||
for ((es, ee), (ns, ne)) in transition_exit_range
|
||
.iter()
|
||
.zip(transition_enter_range.iter())
|
||
{
|
||
let start: u16 = path_data
|
||
.len()
|
||
.try_into()
|
||
.expect("transition path data exceeds u16 offset range");
|
||
path_data.extend_from_slice(&transition_exit_data[*es as usize..*ee as usize]);
|
||
let mid: u16 = path_data
|
||
.len()
|
||
.try_into()
|
||
.expect("transition path data exceeds u16 offset range");
|
||
path_data.extend_from_slice(&transition_enter_data[*ns as usize..*ne as usize]);
|
||
let end: u16 = path_data
|
||
.len()
|
||
.try_into()
|
||
.expect("transition path data exceeds u16 offset range");
|
||
path_range.push((start, mid, end));
|
||
}
|
||
let path_data_len = path_data.len();
|
||
let n_pairs = n_states * n_states;
|
||
let path_data_lit: Vec<_> = path_data.iter().map(|x| quote! { #x }).collect();
|
||
let path_range_lit: Vec<_> = path_range
|
||
.iter()
|
||
.map(|(a, b, c)| quote! { (#a, #b, #c) })
|
||
.collect();
|
||
// Precomputed terminate-path table: for every state, the exit chain
|
||
// up to and including __Root. `do_terminate()` uses this instead of
|
||
// walking __PARENT at runtime.
|
||
let (terminate_data, terminate_range) = compute_terminate_paths(&ir.states);
|
||
let terminate_data_len = terminate_data.len();
|
||
let terminate_data_lit: Vec<_> = terminate_data.iter().map(|x| quote! { #x }).collect();
|
||
let terminate_range_lit: Vec<_> = terminate_range
|
||
.iter()
|
||
.map(|(a, b)| quote! { (#a, #b) })
|
||
.collect();
|
||
|
||
// Precomputed event-dispatch table: for every (state, event_id) pair,
|
||
// the resolved handler that the runtime bubble walk would have found.
|
||
// See `compute_event_dispatch` for encoding.
|
||
let dispatch_table = compute_event_dispatch(ir);
|
||
let n_events = ir.event_variants.len();
|
||
let n_dispatch = n_states * n_events;
|
||
let dispatch_lit: Vec<TokenStream> = dispatch_table
|
||
.iter()
|
||
.map(|(h_state, actions, target)| {
|
||
let actions_lit = actions.iter().map(|a| quote! { #a });
|
||
let target_tok = match target {
|
||
Some(t) => quote! { Some(#t) },
|
||
None => quote! { None },
|
||
};
|
||
quote! { (#h_state, &[#(#actions_lit),*], #target_tok) }
|
||
})
|
||
.collect();
|
||
let state_id_to_variant_arms = ir
|
||
.states
|
||
.iter()
|
||
.filter(|s| s.parent.is_some())
|
||
.map(|s| {
|
||
let id = s.id;
|
||
let nm = &s.name;
|
||
quote! { #id => #state_enum_name::#nm, }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
// Trait methods, possibly with typed params for payload-bearing actions.
|
||
let action_methods_sync = ir
|
||
.actions
|
||
.iter()
|
||
.map(|a| {
|
||
let name = &a.name;
|
||
let params = a.params.iter().map(|f| {
|
||
let n = &f.name;
|
||
let t = &f.ty;
|
||
quote! { #n: #t }
|
||
});
|
||
quote! { fn #name(&mut self, #(#params),*); }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let action_methods_async = ir
|
||
.actions
|
||
.iter()
|
||
.map(|a| {
|
||
let name = &a.name;
|
||
let params = a.params.iter().map(|f| {
|
||
let n = &f.name;
|
||
let t = &f.ty;
|
||
quote! { #n: #t }
|
||
});
|
||
quote! {
|
||
fn #name(&mut self, #(#params),*) -> impl ::core::future::Future<Output = ()>;
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
// Dispatch arms: for unit-signature actions call straight through; for
|
||
// payload-bearing actions, re-match on the passed event and destructure.
|
||
// Payload dispatch binds each field by reference (scrutinee is `&Ev`)
|
||
// then `.clone()`s it to produce an owned value matching the handler's
|
||
// declared signature. For `Copy` types this optimizes away.
|
||
let run_action_arms_sync = ir
|
||
.actions
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, a)| {
|
||
let i = i as u16;
|
||
let name = &a.name;
|
||
if a.params.is_empty() {
|
||
quote! { #i => <Ctx as #actions_trait_name>::#name(ctx), }
|
||
} else {
|
||
let field_idents: Vec<_> = a.params.iter().map(|f| f.name.clone()).collect();
|
||
let clone_args = field_idents.iter().map(|n| quote! { #n.clone() });
|
||
let variant_arms = a.bound_variants.iter().map(|bv| {
|
||
let vname = &bv.name;
|
||
let binds = &field_idents;
|
||
let call = {
|
||
let clones = clone_args.clone();
|
||
quote! { <Ctx as #actions_trait_name>::#name(ctx, #(#clones),*); }
|
||
};
|
||
match bv.kind {
|
||
PayloadKind::Tuple => quote! {
|
||
#ev_ty::#vname(#(#binds),*) => { #call }
|
||
},
|
||
PayloadKind::Struct => quote! {
|
||
#ev_ty::#vname { #(#binds),* } => { #call }
|
||
},
|
||
}
|
||
});
|
||
quote! {
|
||
#i => {
|
||
if let Some(__ev) = __event {
|
||
match __ev {
|
||
#(#variant_arms)*
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let run_action_arms_async = ir
|
||
.actions
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, a)| {
|
||
let i = i as u16;
|
||
let name = &a.name;
|
||
if a.params.is_empty() {
|
||
quote! { #i => <Ctx as #actions_trait_name>::#name(ctx).await, }
|
||
} else {
|
||
let field_idents: Vec<_> = a.params.iter().map(|f| f.name.clone()).collect();
|
||
let clone_args = field_idents.iter().map(|n| quote! { #n.clone() });
|
||
let variant_arms = a.bound_variants.iter().map(|bv| {
|
||
let vname = &bv.name;
|
||
let binds = &field_idents;
|
||
let call = {
|
||
let clones = clone_args.clone();
|
||
quote! { <Ctx as #actions_trait_name>::#name(ctx, #(#clones),*).await; }
|
||
};
|
||
match bv.kind {
|
||
PayloadKind::Tuple => quote! {
|
||
#ev_ty::#vname(#(#binds),*) => { #call }
|
||
},
|
||
PayloadKind::Struct => quote! {
|
||
#ev_ty::#vname { #(#binds),* } => { #call }
|
||
},
|
||
}
|
||
});
|
||
quote! {
|
||
#i => {
|
||
if let Some(__ev) = __event {
|
||
match __ev {
|
||
#(#variant_arms)*
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let duration_expr_arms = ir
|
||
.duration_triggers
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, d)| {
|
||
let i = i as u16;
|
||
let expr = &d.expr;
|
||
quote! { #i => { let __d: ::hsmc::Duration = #expr; __d }, }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let duration_repeat_arms = ir
|
||
.duration_triggers
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, d)| {
|
||
let i = i as u16;
|
||
let repeat = d.repeat;
|
||
quote! { #i => #repeat, }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let event_variant_arms = ir
|
||
.event_variants
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| {
|
||
let i = i as u16;
|
||
let name = &v.name;
|
||
let pat = match v.kind {
|
||
VariantKind::Unit => quote! { #ev_ty::#name },
|
||
VariantKind::Tuple => quote! { #ev_ty::#name(..) },
|
||
VariantKind::Struct => quote! { #ev_ty::#name { .. } },
|
||
};
|
||
quote! { #pat => #i, }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let state_handler_arms = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
use std::collections::BTreeMap;
|
||
#[derive(Default)]
|
||
struct Group {
|
||
action_ids: Vec<u16>,
|
||
transition_target: Option<u16>,
|
||
}
|
||
let mut groups: BTreeMap<(u8, u16), Group> = BTreeMap::new();
|
||
let mut handlers = s.handlers.iter().collect::<Vec<_>>();
|
||
handlers.sort_by_key(|h| h.decl_index);
|
||
for h in handlers {
|
||
let key = match &h.trigger {
|
||
TriggerIr::Event(id, _, _) => (0u8, *id),
|
||
TriggerIr::Duration(id) => (1u8, *id),
|
||
};
|
||
let g = groups.entry(key).or_default();
|
||
match &h.kind {
|
||
HandlerKindIr::Action(aid, _) => g.action_ids.push(*aid),
|
||
HandlerKindIr::Transition(_, Some(tid)) => g.transition_target = Some(*tid),
|
||
HandlerKindIr::Transition(_, None) => {}
|
||
}
|
||
}
|
||
let sid = s.id;
|
||
let inner_arms = groups
|
||
.iter()
|
||
.map(|((kind, tid), g)| {
|
||
let kind = *kind;
|
||
let tid = *tid;
|
||
let actions = g.action_ids.iter().map(|a| quote! { #a });
|
||
let transition_tok = match g.transition_target {
|
||
Some(t) => quote! { Some(#t) },
|
||
None => quote! { None },
|
||
};
|
||
quote! {
|
||
(#kind, #tid) => {
|
||
let actions: &'static [u16] = &[#(#actions),*];
|
||
let target: Option<u16> = #transition_tok;
|
||
return Some((actions, target));
|
||
}
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
quote! {
|
||
#sid => {
|
||
match (trigger_kind, trigger_id) {
|
||
#(#inner_arms)*
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let owned_timers_arms = ir
|
||
.states
|
||
.iter()
|
||
.map(|s| {
|
||
let sid = s.id;
|
||
let ids = s.owned_timers.iter().map(|t| quote! { #t });
|
||
quote! { #sid => &[#(#ids),*], }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let terminate_match = if let Some(tev) = &ir.terminate_event {
|
||
// Use the kind recorded for this variant (unit/tuple/struct).
|
||
let kind = ir
|
||
.event_variants
|
||
.iter()
|
||
.find(|v| v.name == *tev)
|
||
.map(|v| v.kind)
|
||
.unwrap_or(VariantKind::Unit);
|
||
let pat = match kind {
|
||
VariantKind::Unit => quote! { #ev_ty::#tev },
|
||
VariantKind::Tuple => quote! { #ev_ty::#tev(..) },
|
||
VariantKind::Struct => quote! { #ev_ty::#tev { .. } },
|
||
};
|
||
quote! {
|
||
if matches!(__ev, #pat) { return TerminateCheck::Yes; }
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
let _ = n_actions;
|
||
|
||
// Doc strings as literals for quoting.
|
||
let state_enum_doc = format!(
|
||
"Enum of every state declared in the `{}` statechart. Each variant names a user-declared state; the root is implicit and never surfaced.",
|
||
machine_name
|
||
);
|
||
let actions_trait_doc = format!(
|
||
"Trait implemented by the user to provide action function bodies for the `{}` statechart. One method per unique action name.",
|
||
machine_name
|
||
);
|
||
let action_ctx_doc = format!(
|
||
"Wrapper passed to action methods of the `{}` statechart. `Deref`s to the user's context and exposes [`emit()`](#method.emit) for re-entrant event emission (§4.3).",
|
||
machine_name
|
||
);
|
||
let ctx_alias_doc = format!(
|
||
"Convenience type alias for [`{}ActionContext`] (spec §12.4).",
|
||
machine_name
|
||
);
|
||
let machine_doc = format!(
|
||
"The `{}` state machine. Drive it with [`step()`](Self::step) or, under an async feature, `run().await`. See the crate docs for the generated API (§4.4).",
|
||
machine_name
|
||
);
|
||
let sender_doc = format!(
|
||
"Handle for sending events into a running `{}` from other threads, tasks, or ISRs (§4.5).",
|
||
machine_name
|
||
);
|
||
|
||
// Optional-events branch: decide whether to emit Sender / sender() / channel
|
||
// plumbing at all. When events: was omitted the machine is timer-only and
|
||
// has no Sender type.
|
||
let emit_sender = !ir.events_omitted;
|
||
|
||
// Synthesize the empty event type when events: was omitted.
|
||
let no_events_enum = if ir.events_omitted {
|
||
quote! {
|
||
#[doc(hidden)]
|
||
#[allow(dead_code)]
|
||
pub enum __NoEvents {}
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
// Race-loop match arms for each run() variant. When the active state has
|
||
// `during:` activities on its path, the arm constructs those futures by
|
||
// split-borrowing context fields and races them against the channel and
|
||
// next-timer sleep via `tokio::select!` / `embassy_futures::select`.
|
||
let tokio_race_arms_with_ch = emit_tokio_race_arms_with_channel(ir);
|
||
let tokio_race_arms_timer_only = emit_tokio_race_arms_timer_only(ir);
|
||
let embassy_race_arms_with_ch = emit_embassy_race_arms_with_channel(ir);
|
||
let embassy_race_arms_timer_only = emit_embassy_race_arms_timer_only(ir);
|
||
|
||
// Whether any state in the machine declares at least one `during:`. When
|
||
// false, run() uses the original v0.1 pre-during code path verbatim,
|
||
// which keeps backward compatibility bit-for-bit for machines that
|
||
// never opted into the new feature.
|
||
let has_any_durings = ir.states.iter().any(|s| !s.durings.is_empty());
|
||
|
||
// The tokio with-channel run() body. v0.1 verbatim when no durings are
|
||
// declared; otherwise a per-state match dispatching to `select!` with
|
||
// the appropriate active during set.
|
||
let tokio_run_with_ch_body = if has_any_durings {
|
||
quote! {
|
||
#[allow(dead_code)]
|
||
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::tokio::time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
continue;
|
||
}
|
||
let sleep_dur = next.unwrap_or(::hsmc::Duration::from_secs(3600));
|
||
let race: __HsmcRace<#ev_ty> = match self.current {
|
||
#(#tokio_race_arms_with_ch)*
|
||
_ => {
|
||
let sleep = ::tokio::time::sleep(sleep_dur);
|
||
::tokio::pin!(sleep);
|
||
let rx = self.__tokio_rx.as_mut().expect("hsmc: tokio channel");
|
||
::tokio::select! {
|
||
biased;
|
||
maybe = rx.recv() => match maybe {
|
||
Some(ev) => __HsmcRace::Event(ev),
|
||
None => __HsmcRace::ChannelClosed,
|
||
},
|
||
_ = &mut sleep => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
};
|
||
match race {
|
||
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
||
__HsmcRace::ChannelClosed => { if next.is_none() { break; } }
|
||
__HsmcRace::Timer => {}
|
||
}
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
} else {
|
||
quote! {
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::tokio::time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
continue;
|
||
}
|
||
let sleep_dur = next.unwrap_or(::hsmc::Duration::from_secs(3600));
|
||
let sleep = ::tokio::time::sleep(sleep_dur);
|
||
::tokio::pin!(sleep);
|
||
let rx = self.__tokio_rx.as_mut().expect("tokio channel");
|
||
::tokio::select! {
|
||
maybe_ev = rx.recv() => {
|
||
match maybe_ev {
|
||
Some(ev) => { let _ = self.queue.push_back(ev); }
|
||
None => {
|
||
if next.is_none() { break; }
|
||
}
|
||
}
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
}
|
||
_ = &mut sleep => {
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
};
|
||
|
||
// The embassy with-channel run() body. v0.1 verbatim when no durings
|
||
// are declared; otherwise a per-state match dispatching to
|
||
// embassy_futures::select_N with the appropriate active during set.
|
||
let embassy_run_with_ch_body = if has_any_durings {
|
||
quote! {
|
||
#[allow(dead_code)]
|
||
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::embassy_time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
continue;
|
||
}
|
||
let race: __HsmcRace<#ev_ty> = match self.current {
|
||
#(#embassy_race_arms_with_ch)*
|
||
_ => {
|
||
let timer_fut = async {
|
||
match next {
|
||
Some(d) => {
|
||
::embassy_time::Timer::after(
|
||
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
||
).await;
|
||
}
|
||
None => ::core::future::pending::<()>().await,
|
||
}
|
||
};
|
||
let rx = self.__embassy_rx.as_ref().expect("hsmc: embassy channel");
|
||
match ::embassy_futures::select::select(rx.receive(), timer_fut).await {
|
||
::embassy_futures::select::Either::First(ev) => __HsmcRace::Event(ev),
|
||
::embassy_futures::select::Either::Second(()) => __HsmcRace::Timer,
|
||
}
|
||
}
|
||
};
|
||
match race {
|
||
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
||
__HsmcRace::ChannelClosed => {}
|
||
__HsmcRace::Timer => {}
|
||
}
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
} else {
|
||
quote! {
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let rx = self.__embassy_rx
|
||
.expect("hsmc: embassy channel (populated by Machine::new)");
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::embassy_time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
continue;
|
||
}
|
||
let timer_fut = async {
|
||
match next {
|
||
Some(d) => {
|
||
::embassy_time::Timer::after(
|
||
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
||
).await;
|
||
}
|
||
None => ::core::future::pending::<()>().await,
|
||
}
|
||
};
|
||
match ::embassy_futures::select::select(rx.receive(), timer_fut).await {
|
||
::embassy_futures::select::Either::First(ev) => {
|
||
let _ = self.queue.push_back(ev);
|
||
}
|
||
::embassy_futures::select::Either::Second(()) => {}
|
||
}
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
};
|
||
|
||
// State ids whose active `during:` chain is non-empty. Used by timer-only
|
||
// run() bodies to decide whether to exit when no timers are pending: if
|
||
// the current state has an active during, the run loop should keep
|
||
// awaiting rather than break (the during might yet produce an event).
|
||
let states_with_active_durings: Vec<u16> = active_durings_per_innermost_state(ir)
|
||
.into_iter()
|
||
.map(|(sid, _)| sid)
|
||
.collect();
|
||
let has_active_during_arms: Vec<TokenStream> = states_with_active_durings
|
||
.iter()
|
||
.map(|sid| quote! { Some(#sid) => true, })
|
||
.collect();
|
||
|
||
// Human-readable ASCII diagram of the machine's hierarchy. Emitted as a
|
||
// `pub const STATE_CHART: &str` on the machine struct. Useful for
|
||
// `defmt::info!`, panic messages, and documentation.
|
||
let state_chart_str = emit_state_chart_string(ir);
|
||
|
||
// Sender / sender() under tokio. With eager channel creation in
|
||
// `with_queue_capacity_internal()`, `sender()` is `&self`.
|
||
let tokio_sender_impl = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "tokio")]
|
||
#[doc = #sender_doc]
|
||
#[derive(Clone)]
|
||
pub struct #sender_name {
|
||
tx: ::tokio::sync::mpsc::UnboundedSender<#ev_ty>,
|
||
}
|
||
#[cfg(feature = "tokio")]
|
||
impl #sender_name {
|
||
/// Send an event to the machine. Returns `Err(AlreadyTerminated)` if
|
||
/// the machine has terminated and the receiver has been dropped.
|
||
pub fn send(&self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
self.tx.send(event).map_err(|_| ::hsmc::HsmcError::AlreadyTerminated)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "tokio")]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Obtain a clonable, `Send` handle for pushing events into this
|
||
/// machine. The channel is eagerly created in `new()` (§4.4).
|
||
pub fn sender(&self) -> #sender_name {
|
||
#sender_name { tx: self.__tokio_tx.as_ref().expect("hsmc: tokio channel").clone() }
|
||
}
|
||
|
||
/// Run the machine to completion on the current Tokio runtime.
|
||
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
#tokio_run_with_ch_body
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Timer-only: no Sender, no tokio channel, but keep a timer-only run().
|
||
quote! {
|
||
#[cfg(feature = "tokio")]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Run the machine to completion, honouring timers and
|
||
/// `during:` activities. No event channel is generated
|
||
/// because this statechart declares no `events:` (§12.2).
|
||
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
#[allow(dead_code)]
|
||
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::tokio::time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
continue;
|
||
}
|
||
let sleep_dur = match next {
|
||
Some(d) => d,
|
||
None => {
|
||
// No timers pending. If the current state
|
||
// has no `during:` activities either, there
|
||
// is nothing left to wait on — exit.
|
||
let has_any_during: bool = match self.current {
|
||
#(#has_active_during_arms)*
|
||
_ => false,
|
||
};
|
||
if !has_any_during { break; }
|
||
::hsmc::Duration::from_secs(3600)
|
||
}
|
||
};
|
||
let race: __HsmcRace<#ev_ty> = match self.current {
|
||
#(#tokio_race_arms_timer_only)*
|
||
_ => {
|
||
::tokio::time::sleep(sleep_dur).await;
|
||
__HsmcRace::Timer
|
||
}
|
||
};
|
||
match race {
|
||
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
||
__HsmcRace::ChannelClosed => {}
|
||
__HsmcRace::Timer => {}
|
||
}
|
||
let now = ::tokio::time::Instant::now();
|
||
let elapsed = now.duration_since(last_instant);
|
||
last_instant = now;
|
||
next = self.step(elapsed).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Default (no feature): stub sender that documents the limitation. We
|
||
// intentionally keep it non-functional — a proper `Send + Clone` handle
|
||
// without `alloc` or a platform primitive is out of scope for v0.1. See
|
||
// the `tokio` or `embassy` features for a working `sender()`.
|
||
let default_sender_impl = if emit_sender {
|
||
quote! {
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[doc = #sender_doc]
|
||
///
|
||
/// **Limitation:** without the `tokio` or `embassy` feature, the
|
||
/// default sender is a non-functional stub and
|
||
/// [`send()`](Self::send) always returns
|
||
/// [`HsmcError::AlreadyTerminated`]. Use the `tokio` or `embassy`
|
||
/// feature to get a working cross-thread sender.
|
||
#[derive(Clone)]
|
||
pub struct #sender_name { _priv: () }
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
impl #sender_name {
|
||
/// Always returns [`HsmcError::AlreadyTerminated`] under
|
||
/// default features — enable `tokio` or `embassy` for a
|
||
/// functional implementation.
|
||
pub fn send(&self, _event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
Err(::hsmc::HsmcError::AlreadyTerminated)
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Non-functional stub under default features — see
|
||
/// [`#sender_name`] for the limitation.
|
||
pub fn sender(&self) -> #sender_name { #sender_name { _priv: () } }
|
||
}
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
// Embassy: first-class support. `Machine::new(ctx, &'static CHAN)` stores
|
||
// type-erased send/receive endpoints so the Machine struct stays free of
|
||
// a `M: RawMutex` generic. `sender()` hands out a `Copy + Send + Sync`
|
||
// newtype usable from ISRs (`try_send`) and tasks (`send().await`).
|
||
let send_assert_name = format_ident!("__hsmc_embassy_ev_send_assert_{}", machine_name);
|
||
let embassy_sender_impl = if emit_sender {
|
||
quote! {
|
||
// Compile-time assertion: the event type must be `Send` under the
|
||
// embassy feature (its sender handle is `Send + Sync`, which
|
||
// requires `E: Send`). Fails with a pointer to `#ev_ty`.
|
||
#[cfg(feature = "embassy")]
|
||
#[allow(dead_code, non_upper_case_globals)]
|
||
const #send_assert_name: fn() = || {
|
||
fn __assert_send<T: ::core::marker::Send>() {}
|
||
__assert_send::<#ev_ty>();
|
||
};
|
||
|
||
#[cfg(feature = "embassy")]
|
||
#[doc = #sender_doc]
|
||
///
|
||
/// `Copy + Send + Sync`-able handle around a
|
||
/// [`embassy_sync::channel::SendDynamicSender`]. Safe to capture in
|
||
/// a `static` for ISR use and to clone across Embassy tasks.
|
||
#[derive(Clone, Copy)]
|
||
pub struct #sender_name {
|
||
inner: ::embassy_sync::channel::SendDynamicSender<'static, #ev_ty>,
|
||
}
|
||
#[cfg(feature = "embassy")]
|
||
impl #sender_name {
|
||
/// Non-blocking send. Returns
|
||
/// [`HsmcError::QueueFull`] if the channel has no room. Safe
|
||
/// from ISR context when the channel is backed by
|
||
/// `CriticalSectionRawMutex`.
|
||
#[track_caller]
|
||
pub fn try_send(self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
self.inner.try_send(event).map_err(|_| ::hsmc::HsmcError::QueueFull)
|
||
}
|
||
/// Async send. Backpressures (awaits) when the channel is
|
||
/// full. Use from Embassy tasks that must not drop events.
|
||
pub async fn send(self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
self.inner.send(event).await;
|
||
Ok(())
|
||
}
|
||
/// Build a sender directly from a user-declared static
|
||
/// channel — convenient for ISR shims that run before the
|
||
/// machine task has spawned. The channel's event type and
|
||
/// capacity must match the machine.
|
||
pub fn from_channel<__M, const __N: usize>(
|
||
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, __N>,
|
||
) -> Self
|
||
where
|
||
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
||
+ ::core::marker::Sync
|
||
+ ::core::marker::Send
|
||
+ 'static,
|
||
#ev_ty: ::core::marker::Send,
|
||
{
|
||
Self { inner: channel.sender().into() }
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "embassy")]
|
||
impl #machine_name<8> {
|
||
/// Construct a new machine with the default event-queue
|
||
/// capacity of 8, bound to a user-declared static
|
||
/// `embassy_sync::channel::Channel`. The channel's capacity
|
||
/// must match the machine's `__QN` — here, 8.
|
||
pub fn new<__M>(
|
||
ctx: #ctx_ty,
|
||
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, 8>,
|
||
) -> Self
|
||
where
|
||
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
||
+ ::core::marker::Sync
|
||
+ ::core::marker::Send
|
||
+ 'static,
|
||
#ev_ty: ::core::marker::Send,
|
||
{
|
||
let mut __self = <#machine_name<8>>::with_queue_capacity_internal(ctx);
|
||
__self.__embassy_rx = Some(channel.receiver().into());
|
||
__self.__embassy_tx = Some(channel.sender().into());
|
||
__self
|
||
}
|
||
/// Construct a machine with a custom event-queue capacity
|
||
/// `__N`. The channel's capacity must also be `__N`.
|
||
pub fn with_queue_capacity<__M, const __N: usize>(
|
||
ctx: #ctx_ty,
|
||
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, __N>,
|
||
) -> #machine_name<__N>
|
||
where
|
||
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
||
+ ::core::marker::Sync
|
||
+ ::core::marker::Send
|
||
+ 'static,
|
||
#ev_ty: ::core::marker::Send,
|
||
{
|
||
let mut __self = <#machine_name<__N>>::with_queue_capacity_internal(ctx);
|
||
__self.__embassy_rx = Some(channel.receiver().into());
|
||
__self.__embassy_tx = Some(channel.sender().into());
|
||
__self
|
||
}
|
||
/// Construct a machine that is driven only via `send()` +
|
||
/// `step().await`. No external `Channel` is wired up, so
|
||
/// [`sender()`](Self::sender) and [`run()`](Self::run)
|
||
/// panic if called. Use this when the single task that
|
||
/// owns the machine is also its only event producer —
|
||
/// the internal queue is sufficient and a
|
||
/// `&'static Channel` would be dead weight.
|
||
pub fn new_local(ctx: #ctx_ty) -> Self {
|
||
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "embassy")]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Channel-less variant of
|
||
/// [`with_queue_capacity`](Self::with_queue_capacity)
|
||
/// — see [`new_local`](Self::new_local).
|
||
pub fn with_queue_capacity_local(ctx: #ctx_ty) -> #machine_name<__QN> {
|
||
<#machine_name<__QN>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "embassy")]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Obtain a clonable, `Send` handle for pushing events into
|
||
/// this machine from ISRs or other Embassy tasks. The channel
|
||
/// was provided at construction.
|
||
pub fn sender(&self) -> #sender_name {
|
||
#sender_name {
|
||
inner: self.__embassy_tx
|
||
.expect("hsmc: embassy channel (populated by Machine::new)"),
|
||
}
|
||
}
|
||
|
||
/// Run the machine to completion, awaiting events from the
|
||
/// bound channel, firing timers via `embassy_time`, and
|
||
/// polling any active `during:` activities for the current
|
||
/// state path. Returns `Ok(())` on clean termination;
|
||
/// `Err(HsmcError::QueueFull)` if an action's `emit()`
|
||
/// overflowed the internal queue.
|
||
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
#embassy_run_with_ch_body
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Timer-only machines (no `events:` declared) have no channel. Keep
|
||
// the existing timer-driven run().
|
||
quote! {
|
||
#[cfg(feature = "embassy")]
|
||
impl #machine_name<8> {
|
||
/// Construct a timer-only machine (no `events:` declared).
|
||
pub fn new(ctx: #ctx_ty) -> Self {
|
||
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
/// Construct a timer-only machine with custom queue capacity.
|
||
pub fn with_queue_capacity<const __N: usize>(ctx: #ctx_ty) -> #machine_name<__N> {
|
||
<#machine_name<__N>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "embassy")]
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// Timer-only run loop (no `events:` declared) using
|
||
/// `embassy_time::Timer`, plus any `during:` activities
|
||
/// declared on the active state path.
|
||
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
#[allow(dead_code)]
|
||
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
let mut last_instant = ::embassy_time::Instant::now();
|
||
while !self.terminated {
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if !self.queue.is_empty() {
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
continue;
|
||
}
|
||
// No queued events. Decide whether to wait.
|
||
let has_any_during: bool = match self.current {
|
||
#(#has_active_during_arms)*
|
||
_ => false,
|
||
};
|
||
if next.is_none() && !has_any_during { break; }
|
||
let race: __HsmcRace<#ev_ty> = match self.current {
|
||
#(#embassy_race_arms_timer_only)*
|
||
_ => {
|
||
// No durings active; just await the timer.
|
||
match next {
|
||
Some(d) => {
|
||
::embassy_time::Timer::after(
|
||
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
||
).await;
|
||
}
|
||
None => ::core::future::pending::<()>().await,
|
||
}
|
||
__HsmcRace::Timer
|
||
}
|
||
};
|
||
match race {
|
||
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
||
__HsmcRace::ChannelClosed => {}
|
||
__HsmcRace::Timer => {}
|
||
}
|
||
let now = ::embassy_time::Instant::now();
|
||
let elapsed_us = now.duration_since(last_instant).as_micros();
|
||
last_instant = now;
|
||
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Embassy stored receiver/sender fields. Populated at construction time
|
||
// when the user provides an `&'static embassy_sync::channel::Channel`.
|
||
let embassy_fields = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "embassy")]
|
||
__embassy_rx: Option<::embassy_sync::channel::SendDynamicReceiver<'static, #ev_ty>>,
|
||
#[cfg(feature = "embassy")]
|
||
__embassy_tx: Option<::embassy_sync::channel::SendDynamicSender<'static, #ev_ty>>,
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
let embassy_ctor = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "embassy")]
|
||
__embassy_rx: None,
|
||
#[cfg(feature = "embassy")]
|
||
__embassy_tx: None,
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
// Eager tokio channel fields. Always emitted when `tokio` feature is
|
||
// compiled in AND this machine has events.
|
||
let tokio_fields = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "tokio")]
|
||
__tokio_tx: Option<::tokio::sync::mpsc::UnboundedSender<#ev_ty>>,
|
||
#[cfg(feature = "tokio")]
|
||
__tokio_rx: Option<::tokio::sync::mpsc::UnboundedReceiver<#ev_ty>>,
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
let tokio_ctor = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "tokio")]
|
||
__tokio_tx: { let (tx, _rx) = ::tokio::sync::mpsc::unbounded_channel(); Some(tx) },
|
||
#[cfg(feature = "tokio")]
|
||
__tokio_rx: None,
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
// We need to create tx+rx together; do it via a small helper.
|
||
let tokio_ctor_setup = if emit_sender {
|
||
quote! {
|
||
#[cfg(feature = "tokio")]
|
||
{
|
||
let (tx, rx) = ::tokio::sync::mpsc::unbounded_channel();
|
||
__self.__tokio_tx = Some(tx);
|
||
__self.__tokio_rx = Some(rx);
|
||
}
|
||
}
|
||
} else {
|
||
quote! {}
|
||
};
|
||
|
||
quote! {
|
||
#no_events_enum
|
||
|
||
#[doc = #state_enum_doc]
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
#[allow(non_camel_case_types, dead_code)]
|
||
pub enum #state_enum_name {
|
||
#(#state_variants),*
|
||
}
|
||
|
||
#[doc = #actions_trait_doc]
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[allow(non_snake_case)]
|
||
pub trait #actions_trait_name {
|
||
#(#action_methods_sync)*
|
||
}
|
||
|
||
#[doc = #actions_trait_doc]
|
||
///
|
||
/// Under the `tokio` or `embassy` feature, action methods are
|
||
/// `async fn` so that entry/exit/event-action bodies can `.await`
|
||
/// peripheral calls (I2C flush, radio ops, etc.). Returned futures
|
||
/// must be `Send` for compatibility with multi-threaded / Send
|
||
/// executors.
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
#[allow(non_snake_case)]
|
||
pub trait #actions_trait_name {
|
||
#(#action_methods_async)*
|
||
}
|
||
|
||
#[doc = #action_ctx_doc]
|
||
pub struct #action_ctx_name<'a> {
|
||
ctx: &'a mut #ctx_ty,
|
||
__queue: &'a mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send),
|
||
__terminated: &'a bool,
|
||
__journal: &'a mut ::hsmc::__private::JournalSink,
|
||
}
|
||
|
||
#[doc = #ctx_alias_doc]
|
||
pub type #ctx_alias_name<'a> = #action_ctx_name<'a>;
|
||
|
||
impl<'a> ::core::ops::Deref for #action_ctx_name<'a> {
|
||
type Target = #ctx_ty;
|
||
fn deref(&self) -> &#ctx_ty { self.ctx }
|
||
}
|
||
|
||
impl<'a> ::core::ops::DerefMut for #action_ctx_name<'a> {
|
||
fn deref_mut(&mut self) -> &mut #ctx_ty { self.ctx }
|
||
}
|
||
|
||
impl<'a> #action_ctx_name<'a> {
|
||
/// Push an event into the internal queue. It will be processed
|
||
/// after the current event's handling completes (§2.12). Returns
|
||
/// [`HsmcError::QueueFull`] if the queue is full, or
|
||
/// [`HsmcError::AlreadyTerminated`] if the machine is shutting
|
||
/// down.
|
||
#[allow(unused_variables)]
|
||
pub fn emit(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
if *self.__terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
let __ev_id = __event_to_id(&event);
|
||
let __result = self.__queue.push(event);
|
||
if __result.is_ok() {
|
||
::hsmc::__chart_observe!(
|
||
EmitQueued,
|
||
&mut *self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id, __event_name(__ev_id)
|
||
);
|
||
} else {
|
||
::hsmc::__chart_observe!(
|
||
EmitFailed,
|
||
&mut *self.__journal,
|
||
#chart_name_lit,
|
||
__ev_id, __event_name(__ev_id)
|
||
);
|
||
}
|
||
__result
|
||
}
|
||
|
||
/// Convenience wrapper around [`emit()`](Self::emit) that panics on
|
||
/// failure (§12.1). Use in contexts where a full queue is a bug
|
||
/// rather than a recoverable condition.
|
||
pub fn emit_or_panic(&mut self, event: #ev_ty) {
|
||
self.emit(event).expect("hsmc: event queue full");
|
||
}
|
||
}
|
||
|
||
#[doc = #machine_doc]
|
||
#[allow(non_snake_case, non_camel_case_types)]
|
||
pub struct #machine_name<const __QN: usize = 8> {
|
||
ctx: #ctx_ty,
|
||
current: Option<u16>,
|
||
queue: ::hsmc::__private::heapless::Deque<#ev_ty, __QN>,
|
||
timers: ::hsmc::__private::TimerTable<#max_timers>,
|
||
terminated: bool,
|
||
__overflow: bool,
|
||
/// Deterministic execution journal. ZST when the `hsmc/journal`
|
||
/// feature is off; a `Vec<TraceEvent>` when on.
|
||
__journal: ::hsmc::__private::JournalSink,
|
||
#tokio_fields
|
||
#embassy_fields
|
||
}
|
||
|
||
const __PARENT: [Option<u16>; #n_states] = [#(#parent_table),*];
|
||
const __DEPTH: [u8; #n_states] = [#(#depth_table),*];
|
||
const __DEFAULT_CHILD: [Option<u16>; #n_states] = [#(#default_child_table),*];
|
||
|
||
// ── Precomputed transition paths ────────────────────────────
|
||
//
|
||
// For every (from, to) state pair, the exit-then-enter sequence
|
||
// a transition would walk is materialized at codegen time. The
|
||
// dispatch hot path (`fn transition`) becomes one range lookup
|
||
// and two straight loops; the runtime LCA depth-walk and the
|
||
// heapless::Vec ancestry construction are gone.
|
||
//
|
||
// Encoding: `__PATH_DATA` is the concatenation of every
|
||
// `[exit; enter]` slice. `__PATH_RANGE[i*N + j] = (s, m, e)`
|
||
// gives the boundaries: `exit = data[s..m]`, `enter = data[m..e]`.
|
||
// Empty enter slice (`m == e`) ⇒ up-transition, target excluded;
|
||
// otherwise the enter slice already includes the default-descent
|
||
// tail so no separate `descend_defaults()` call is needed.
|
||
//
|
||
// Folding both ranges into one struct halves the cold-cache
|
||
// line touches on first dispatch — the up-transition hot path
|
||
// does one indexed load instead of two.
|
||
const __PATH_DATA: [u16; #path_data_len] = [#(#path_data_lit),*];
|
||
const __PATH_RANGE: [(u16, u16, u16); #n_pairs] = [#(#path_range_lit),*];
|
||
|
||
// ── Precomputed terminate exit paths ───────────────────────
|
||
//
|
||
// For every starting state, the full exit chain up to and
|
||
// including __Root. `do_terminate()` indexes into this instead
|
||
// of walking __PARENT at runtime.
|
||
const __TERMINATE_DATA: [u16; #terminate_data_len] = [#(#terminate_data_lit),*];
|
||
const __TERMINATE_RANGE: [(u16, u16); #n_states] = [#(#terminate_range_lit),*];
|
||
|
||
// ── Precomputed event-dispatch table ────────────────────────
|
||
//
|
||
// For every (current_state, event_id) pair, the deepest ancestor
|
||
// state with an `on(EventX)` clause is resolved at codegen time.
|
||
// The runtime `step()` event-handling block does one indexed
|
||
// lookup instead of an `__PARENT` bubble walk + `__handler_lookup`
|
||
// call per ancestor.
|
||
//
|
||
// Entry layout: (handler_state, action_ids, transition_target).
|
||
// `handler_state == u16::MAX` is the no-handler sentinel — the
|
||
// event would have been dropped at runtime. The dropped/delivered
|
||
// observation arms key off that sentinel, preserving the existing
|
||
// journal vocabulary.
|
||
const __DISPATCH: [(u16, &'static [u16], Option<u16>); #n_dispatch] =
|
||
[#(#dispatch_lit),*];
|
||
|
||
#name_helpers
|
||
|
||
#[allow(dead_code, unreachable_patterns)]
|
||
fn __state_id_to_public(id: u16) -> #state_enum_name {
|
||
match id {
|
||
#(#state_id_to_variant_arms)*
|
||
_ => panic!("internal: state id {} has no public variant", id),
|
||
}
|
||
}
|
||
|
||
// Hot-path lookup helpers. `#[inline]` so LLVM folds the
|
||
// (typically jump-table) match directly into `step()` /
|
||
// `transition()` / `exit_state()`, removing a call+return per
|
||
// dispatched event and cutting one stack frame.
|
||
#[allow(unreachable_code, unused_variables)]
|
||
#[inline]
|
||
fn __duration_for(tid: u16) -> ::hsmc::Duration {
|
||
match tid {
|
||
#(#duration_expr_arms)*
|
||
_ => ::hsmc::Duration::ZERO,
|
||
}
|
||
}
|
||
|
||
#[allow(unreachable_code, unused_variables)]
|
||
#[inline]
|
||
fn __duration_repeats(tid: u16) -> bool {
|
||
match tid {
|
||
#(#duration_repeat_arms)*
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
#[allow(unreachable_patterns, unused_variables)]
|
||
#[inline]
|
||
fn __event_to_id(ev: &#ev_ty) -> u16 {
|
||
match ev {
|
||
#(#event_variant_arms)*
|
||
_ => u16::MAX,
|
||
}
|
||
}
|
||
|
||
#[allow(unused_variables)]
|
||
#[inline]
|
||
fn __handler_lookup(
|
||
state_id: u16,
|
||
trigger_kind: u8,
|
||
trigger_id: u16,
|
||
) -> Option<(&'static [u16], Option<u16>)> {
|
||
match state_id {
|
||
#(#state_handler_arms)*
|
||
_ => {}
|
||
}
|
||
None
|
||
}
|
||
|
||
#[allow(unused_variables)]
|
||
#[inline]
|
||
fn __owned_timers(state_id: u16) -> &'static [u16] {
|
||
match state_id {
|
||
#(#owned_timers_arms)*
|
||
_ => &[],
|
||
}
|
||
}
|
||
|
||
enum TerminateCheck { Yes, No }
|
||
|
||
#[allow(unused_variables)]
|
||
#[inline]
|
||
fn __check_terminate(__ev: &#ev_ty) -> TerminateCheck {
|
||
#terminate_match
|
||
TerminateCheck::No
|
||
}
|
||
|
||
impl #machine_name<8> {
|
||
/// Construct a new machine with the default event-queue capacity of 8 (§4.4).
|
||
#[cfg(not(feature = "embassy"))]
|
||
pub fn new(ctx: #ctx_ty) -> Self {
|
||
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
/// Construct a machine with a custom event-queue capacity `__N` (§5.2).
|
||
#[cfg(not(feature = "embassy"))]
|
||
pub fn with_queue_capacity<const __N: usize>(ctx: #ctx_ty) -> #machine_name<__N> {
|
||
<#machine_name<__N>>::with_queue_capacity_internal(ctx)
|
||
}
|
||
}
|
||
|
||
impl<const __QN: usize> #machine_name<__QN> {
|
||
/// ASCII diagram of the state hierarchy, default children, entry/
|
||
/// exit actions, `during:` activities, and handler triggers.
|
||
/// Intended for `defmt::info!`, panic messages, and docs.
|
||
///
|
||
/// Accessible as `Self::STATE_CHART` from any impl block on the
|
||
/// machine; externally requires the turbofish (for example
|
||
/// `Machine::<8>::STATE_CHART`) since the machine carries a const
|
||
/// generic queue capacity.
|
||
pub const STATE_CHART: &'static str = #state_chart_str;
|
||
|
||
fn with_queue_capacity_internal(ctx: #ctx_ty) -> Self {
|
||
#[allow(unused_mut)]
|
||
let mut __self = Self {
|
||
ctx,
|
||
current: None,
|
||
queue: ::hsmc::__private::heapless::Deque::new(),
|
||
timers: ::hsmc::__private::TimerTable::new(),
|
||
terminated: false,
|
||
__overflow: false,
|
||
__journal: ::hsmc::__private::JournalSink::new(),
|
||
#tokio_ctor
|
||
#embassy_ctor
|
||
};
|
||
#tokio_ctor_setup
|
||
__self
|
||
}
|
||
|
||
/// Returns and clears the internal-emit overflow flag. Set by
|
||
/// the generated action dispatch when `emit()` inside an action
|
||
/// fails to enqueue — `run()` consults this to surface
|
||
/// [`HsmcError::QueueFull`] out of the async loop rather than
|
||
/// silently dropping the event. Exposed for callers driving the
|
||
/// machine via `step()` directly.
|
||
pub fn take_overflow(&mut self) -> bool {
|
||
let v = self.__overflow;
|
||
self.__overflow = false;
|
||
v
|
||
}
|
||
|
||
/// Returns `true` once the machine has processed its `terminate` event (§4.4).
|
||
pub fn is_terminated(&self) -> bool { self.terminated }
|
||
|
||
/// Return the innermost active state. Panics if called before the
|
||
/// first `step()` (§4.4).
|
||
pub fn current_state(&self) -> #state_enum_name {
|
||
__state_id_to_public(self.current.expect("machine not started; call step() first"))
|
||
}
|
||
|
||
/// Returns `true` when internal event queue has pending events
|
||
/// (§12.3). Useful for schedulers that want to poll `step()`
|
||
/// again eagerly.
|
||
pub fn has_pending_events(&self) -> bool {
|
||
!self.queue.is_empty()
|
||
}
|
||
|
||
/// Consume the machine and yield the user-provided context (§4.4).
|
||
pub fn into_context(self) -> #ctx_ty { self.ctx }
|
||
|
||
/// Borrow the user-provided context (extension beyond spec §4.4; provided for test convenience).
|
||
pub fn context(&self) -> &#ctx_ty { &self.ctx }
|
||
/// Mutably borrow the user-provided context (extension beyond spec §4.4; provided for test convenience).
|
||
pub fn context_mut(&mut self) -> &mut #ctx_ty { &mut self.ctx }
|
||
|
||
/// Borrow the deterministic execution journal. Only available
|
||
/// under `feature = "journal"`. Returns the in-order sequence
|
||
/// of every observable atom — entries, exits, action calls,
|
||
/// during start/cancel, timer arm/cancel/fire, queued emits,
|
||
/// event delivery, transitions, and termination.
|
||
#[cfg(feature = "journal")]
|
||
pub fn journal(&self) -> &[::hsmc::TraceEvent] {
|
||
self.__journal.events()
|
||
}
|
||
|
||
/// Take and clear the deterministic execution journal. Useful
|
||
/// at test boundaries to compare against an expected sequence
|
||
/// without lingering events from earlier runs.
|
||
#[cfg(feature = "journal")]
|
||
pub fn take_journal(&mut self) -> ::hsmc::Journal {
|
||
self.__journal.take()
|
||
}
|
||
|
||
/// Drop all journal events without returning them.
|
||
#[cfg(feature = "journal")]
|
||
pub fn clear_journal(&mut self) {
|
||
self.__journal.clear();
|
||
}
|
||
|
||
/// Stable identifier for this chart's structural definition.
|
||
/// A different `CHART_HASH` means the chart is a different
|
||
/// machine — replay against a journal recorded for a
|
||
/// different `CHART_HASH` is meaningless and the test driver
|
||
/// should refuse to compare.
|
||
pub const CHART_HASH: u64 = #chart_hash_lit;
|
||
|
||
/// Returns `CHART_HASH` (instance-method form for symmetry
|
||
/// with other introspection methods).
|
||
pub const fn chart_hash(&self) -> u64 { Self::CHART_HASH }
|
||
|
||
/// Advance the machine by `elapsed` wall-clock time. Returns the
|
||
/// duration until the next timer fires, or `None` if the machine
|
||
/// has no live timers (§4.4).
|
||
///
|
||
/// Under the `tokio` or `embassy` feature, `step()` is `async`
|
||
/// because action methods are `async fn` and must be `.await`ed
|
||
/// during dispatch. Under default features it stays sync.
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[allow(unused_variables)]
|
||
pub fn step(&mut self, elapsed: ::hsmc::Duration) -> Option<::hsmc::Duration> {
|
||
if self.terminated { return None; }
|
||
|
||
if self.current.is_none() {
|
||
#observe_started
|
||
self.enter_path(0);
|
||
return #timer_min_remaining_expr;
|
||
}
|
||
|
||
#timer_decrement_call
|
||
|
||
#timer_pop_block_sync
|
||
|
||
if let Some(ev) = self.queue.pop_front() {
|
||
let __ev_id = __event_to_id(&ev);
|
||
#observe_event_received
|
||
if matches!(__check_terminate(&ev), TerminateCheck::Yes) {
|
||
#observe_terminate_requested
|
||
self.do_terminate();
|
||
return None;
|
||
}
|
||
if __ev_id != u16::MAX {
|
||
// Resolve handler via precomputed __DISPATCH table:
|
||
// one indexed lookup replaces the runtime __PARENT
|
||
// bubble walk + per-hop __handler_lookup() call.
|
||
// u16::MAX in the handler_state slot is the
|
||
// no-handler sentinel.
|
||
if let Some(__cur) = self.current {
|
||
let __dispatch_idx =
|
||
(__cur as usize) * #n_events + (__ev_id as usize);
|
||
let (__handler_state, __actions, __target) =
|
||
__DISPATCH[__dispatch_idx];
|
||
if __handler_state != u16::MAX {
|
||
#observe_event_delivered
|
||
self.run_handlers(
|
||
__handler_state, __actions, __target, Some(&ev),
|
||
1u8, __ev_id, 0u16,
|
||
);
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
return #timer_min_remaining_expr;
|
||
}
|
||
|
||
#timer_min_remaining_expr
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
#[allow(unused_variables)]
|
||
pub async fn step(&mut self, elapsed: ::hsmc::Duration) -> Option<::hsmc::Duration> {
|
||
if self.terminated { return None; }
|
||
|
||
if self.current.is_none() {
|
||
#observe_started
|
||
self.enter_path(0).await;
|
||
return #timer_min_remaining_expr;
|
||
}
|
||
|
||
#timer_decrement_call
|
||
|
||
#timer_pop_block_async
|
||
|
||
if let Some(ev) = self.queue.pop_front() {
|
||
let __ev_id = __event_to_id(&ev);
|
||
#observe_event_received
|
||
if matches!(__check_terminate(&ev), TerminateCheck::Yes) {
|
||
#observe_terminate_requested
|
||
self.do_terminate().await;
|
||
return None;
|
||
}
|
||
if __ev_id != u16::MAX {
|
||
// Same precomputed __DISPATCH lookup as the sync
|
||
// branch; see that copy for the encoding notes.
|
||
if let Some(__cur) = self.current {
|
||
let __dispatch_idx =
|
||
(__cur as usize) * #n_events + (__ev_id as usize);
|
||
let (__handler_state, __actions, __target) =
|
||
__DISPATCH[__dispatch_idx];
|
||
if __handler_state != u16::MAX {
|
||
#observe_event_delivered
|
||
self.run_handlers(
|
||
__handler_state, __actions, __target, Some(&ev),
|
||
1u8, __ev_id, 0u16,
|
||
).await;
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
} else {
|
||
#observe_event_dropped
|
||
}
|
||
return #timer_min_remaining_expr;
|
||
}
|
||
|
||
#timer_min_remaining_expr
|
||
}
|
||
|
||
/// Push an event directly into the machine's queue (synchronous,
|
||
/// single-threaded). Extension beyond spec §4.4 — the canonical
|
||
/// cross-thread path is [`sender().send()`](Self::sender). Useful
|
||
/// for tests and tight `step()` driver loops.
|
||
#[doc(hidden)]
|
||
pub fn send(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
self.queue.push_back(event).map_err(|_| ::hsmc::HsmcError::QueueFull)
|
||
}
|
||
|
||
/// Push an event into the internal queue and drain the queue to
|
||
/// quiescence before returning. Under an async feature the step
|
||
/// loop awaits action futures; after `dispatch()` returns,
|
||
/// `current_state()` reflects the post-dispatch state. Use this
|
||
/// from the task that owns the machine when you want "event
|
||
/// processed" semantics (a drop-in replacement for the
|
||
/// `send + drain` pattern). For cross-task / ISR injection use
|
||
/// [`sender()`](Self::sender) instead.
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
pub async fn dispatch(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
||
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
||
self.queue.push_back(event).map_err(|_| ::hsmc::HsmcError::QueueFull)?;
|
||
// Prime state on first call.
|
||
if self.current.is_none() {
|
||
let _ = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if self.terminated { return Ok(()); }
|
||
}
|
||
while !self.queue.is_empty() {
|
||
let _ = self.step(::hsmc::Duration::ZERO).await;
|
||
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
||
if self.terminated { return Ok(()); }
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
// Reason kind passed by callers to `run_handlers`:
|
||
// 1 → event-driven (reason_a = event id)
|
||
// 2 → timer-driven (reason_a = state id, reason_b = timer id)
|
||
// 0 → internal/other (unused today; reserved)
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[allow(unused_variables, clippy::too_many_arguments)]
|
||
fn run_handlers(
|
||
&mut self,
|
||
source_state: u16,
|
||
actions: &'static [u16],
|
||
target: Option<u16>,
|
||
event: Option<&#ev_ty>,
|
||
__reason_kind: u8,
|
||
__reason_a: u16,
|
||
__reason_b: u16,
|
||
) {
|
||
let __handler_state = source_state;
|
||
for &aid in actions {
|
||
#observe_action_handler
|
||
self.run_action(aid, event);
|
||
if self.terminated { return; }
|
||
}
|
||
if let Some(target) = target {
|
||
match __reason_kind {
|
||
1 => {
|
||
let __ev_id = __reason_a;
|
||
#observe_transition_fired_event
|
||
}
|
||
2 => {
|
||
let __t_state = __reason_a;
|
||
let __t_trigger = __reason_b;
|
||
#observe_transition_fired_timer
|
||
}
|
||
_ => {}
|
||
}
|
||
let __t_complete_from = self.current;
|
||
self.transition(source_state, target);
|
||
#observe_transition_complete
|
||
}
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
#[allow(unused_variables, clippy::too_many_arguments)]
|
||
async fn run_handlers(
|
||
&mut self,
|
||
source_state: u16,
|
||
actions: &'static [u16],
|
||
target: Option<u16>,
|
||
event: Option<&#ev_ty>,
|
||
__reason_kind: u8,
|
||
__reason_a: u16,
|
||
__reason_b: u16,
|
||
) {
|
||
let __handler_state = source_state;
|
||
for &aid in actions {
|
||
#observe_action_handler
|
||
self.run_action(aid, event).await;
|
||
if self.terminated { return; }
|
||
}
|
||
if let Some(target) = target {
|
||
match __reason_kind {
|
||
1 => {
|
||
let __ev_id = __reason_a;
|
||
#observe_transition_fired_event
|
||
}
|
||
2 => {
|
||
let __t_state = __reason_a;
|
||
let __t_trigger = __reason_b;
|
||
#observe_transition_fired_timer
|
||
}
|
||
_ => {}
|
||
}
|
||
let __t_complete_from = self.current;
|
||
self.transition(source_state, target).await;
|
||
#observe_transition_complete
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn run_action(&mut self, aid: u16, event: Option<&#ev_ty>) {
|
||
// Trace + journal emission for action calls now happens at
|
||
// the call sites (`#observe_action_entry`,
|
||
// `#observe_action_exit`, `#observe_action_handler`) so the
|
||
// ActionKind tag and call-site state id can be recorded
|
||
// accurately. `run_action` itself is just dispatch.
|
||
let ctx = &mut self.ctx;
|
||
let mut __proxy = ::hsmc::__private::EmitProxy {
|
||
queue: &mut self.queue,
|
||
overflow: &mut self.__overflow,
|
||
};
|
||
let queue: &mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send) = &mut __proxy;
|
||
let terminated = &self.terminated;
|
||
let journal = &mut self.__journal;
|
||
let mut actx = #action_ctx_name {
|
||
ctx,
|
||
__queue: queue,
|
||
__terminated: terminated,
|
||
__journal: journal,
|
||
};
|
||
fn __dispatch<Ctx: #actions_trait_name>(
|
||
ctx: &mut Ctx,
|
||
aid: u16,
|
||
__event: Option<&#ev_ty>,
|
||
) {
|
||
let _ = __event;
|
||
match aid {
|
||
#(#run_action_arms_sync)*
|
||
_ => {}
|
||
}
|
||
}
|
||
__dispatch(&mut actx, aid, event);
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn run_action(&mut self, aid: u16, event: Option<&#ev_ty>) {
|
||
// See sync run_action above — observation happens at call sites.
|
||
let ctx = &mut self.ctx;
|
||
let mut __proxy = ::hsmc::__private::EmitProxy {
|
||
queue: &mut self.queue,
|
||
overflow: &mut self.__overflow,
|
||
};
|
||
let queue: &mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send) = &mut __proxy;
|
||
let terminated = &self.terminated;
|
||
let journal = &mut self.__journal;
|
||
let mut actx = #action_ctx_name {
|
||
ctx,
|
||
__queue: queue,
|
||
__terminated: terminated,
|
||
__journal: journal,
|
||
};
|
||
async fn __dispatch<Ctx: #actions_trait_name>(
|
||
ctx: &mut Ctx,
|
||
aid: u16,
|
||
__event: Option<&#ev_ty>,
|
||
) {
|
||
let _ = __event;
|
||
match aid {
|
||
#(#run_action_arms_async)*
|
||
_ => {}
|
||
}
|
||
}
|
||
__dispatch(&mut actx, aid, event).await;
|
||
}
|
||
|
||
// §2.6 transition shapes (up / self / lateral) are folded
|
||
// into the precomputed path tables emitted at codegen time.
|
||
// An empty enter slice (`m == e`) means up-transition: target
|
||
// is already on the active path, must not be re-entered, and
|
||
// `self.current` is set to `target` after the exit loop. A
|
||
// non-empty enter slice ends at the target itself —
|
||
// `enter_state_no_descent` sets `self.current` as it goes.
|
||
// Default-following runs after the path apply via
|
||
// `follow_defaults`, which fires further transitions for any
|
||
// chained `default(...)` declarations on the just-entered
|
||
// state. The compile-time default-graph cycle check
|
||
// guarantees the chain terminates.
|
||
// Returns true if any state was newly entered (i.e., the
|
||
// enter_path was non-empty). False on an up-transition, where
|
||
// the target was already on the active path.
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn apply_path(&mut self, from: u16, target: u16) -> bool {
|
||
let idx = (from as usize) * #n_states + (target as usize);
|
||
let (s, m, e) = __PATH_RANGE[idx];
|
||
let exit_path = &__PATH_DATA[s as usize..m as usize];
|
||
let enter_path = &__PATH_DATA[m as usize..e as usize];
|
||
for &sid in exit_path {
|
||
self.exit_state(sid);
|
||
}
|
||
for &sid in enter_path {
|
||
self.enter_state_no_descent(sid);
|
||
if self.terminated { return !enter_path.is_empty(); }
|
||
}
|
||
if enter_path.is_empty() {
|
||
self.current = Some(target);
|
||
false
|
||
} else {
|
||
true
|
||
}
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn apply_path(&mut self, from: u16, target: u16) -> bool {
|
||
let idx = (from as usize) * #n_states + (target as usize);
|
||
let (s, m, e) = __PATH_RANGE[idx];
|
||
let exit_path = &__PATH_DATA[s as usize..m as usize];
|
||
let enter_path = &__PATH_DATA[m as usize..e as usize];
|
||
for &sid in exit_path {
|
||
self.exit_state(sid).await;
|
||
}
|
||
for &sid in enter_path {
|
||
self.enter_state_no_descent(sid).await;
|
||
if self.terminated { return !enter_path.is_empty(); }
|
||
}
|
||
if enter_path.is_empty() {
|
||
self.current = Some(target);
|
||
false
|
||
} else {
|
||
true
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn transition(&mut self, _source_matched_state: u16, target: u16) {
|
||
let from = self.current.unwrap_or(target);
|
||
let entered = self.apply_path(from, target);
|
||
if self.terminated { return; }
|
||
if entered {
|
||
self.follow_defaults();
|
||
}
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn transition(&mut self, _source_matched_state: u16, target: u16) {
|
||
let from = self.current.unwrap_or(target);
|
||
let entered = self.apply_path(from, target).await;
|
||
if self.terminated { return; }
|
||
if entered {
|
||
self.follow_defaults().await;
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn enter_path(&mut self, root: u16) {
|
||
self.enter_state_no_descent(root);
|
||
if self.terminated { return; }
|
||
self.follow_defaults();
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn enter_path(&mut self, root: u16) {
|
||
self.enter_state_no_descent(root).await;
|
||
if self.terminated { return; }
|
||
self.follow_defaults().await;
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[allow(unused_variables)]
|
||
fn enter_state_no_descent(&mut self, sid: u16) {
|
||
#observe_enter_began
|
||
let entries = Self::entries_of(sid);
|
||
for &aid in entries {
|
||
#observe_action_entry
|
||
self.run_action(aid, None);
|
||
if self.terminated { return; }
|
||
}
|
||
for &tid in __owned_timers(sid) {
|
||
let d = __duration_for(tid);
|
||
self.timers.start(sid, tid, d);
|
||
#observe_timer_armed
|
||
}
|
||
self.current = Some(sid);
|
||
#observe_durings_started
|
||
#observe_entered
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
#[allow(unused_variables)]
|
||
async fn enter_state_no_descent(&mut self, sid: u16) {
|
||
#observe_enter_began
|
||
let entries = Self::entries_of(sid);
|
||
for &aid in entries {
|
||
#observe_action_entry
|
||
self.run_action(aid, None).await;
|
||
if self.terminated { return; }
|
||
}
|
||
for &tid in __owned_timers(sid) {
|
||
let d = __duration_for(tid);
|
||
self.timers.start(sid, tid, d);
|
||
#observe_timer_armed
|
||
}
|
||
self.current = Some(sid);
|
||
#observe_durings_started
|
||
#observe_entered
|
||
}
|
||
|
||
// After a state's entries finish (initial entry or any
|
||
// transition), follow its `default(...)` chain by firing
|
||
// further transitions. Each step is a full LCA-aware
|
||
// transition because the default target may be a sibling,
|
||
// ancestor, or anywhere else — not just a direct child. The
|
||
// compile-time default-graph cycle check guarantees the loop
|
||
// terminates.
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn follow_defaults(&mut self) {
|
||
while let Some(cur) = self.current {
|
||
let target = match __DEFAULT_CHILD[cur as usize] {
|
||
Some(t) => t,
|
||
None => return,
|
||
};
|
||
#observe_transition_fired_internal
|
||
let __t_complete_from = self.current;
|
||
let entered = self.apply_path(cur, target);
|
||
#observe_transition_complete
|
||
if self.terminated { return; }
|
||
// If the default fired an up-transition (no new state
|
||
// entered), don't keep chaining — the ancestor we
|
||
// landed on was already on the active path; its
|
||
// default was not "freshly entered" and must not fire.
|
||
if !entered { return; }
|
||
}
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn follow_defaults(&mut self) {
|
||
while let Some(cur) = self.current {
|
||
let target = match __DEFAULT_CHILD[cur as usize] {
|
||
Some(t) => t,
|
||
None => return,
|
||
};
|
||
#observe_transition_fired_internal
|
||
let __t_complete_from = self.current;
|
||
let entered = self.apply_path(cur, target).await;
|
||
#observe_transition_complete
|
||
if self.terminated { return; }
|
||
if !entered { return; }
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
#[allow(unused_variables)]
|
||
fn exit_state(&mut self, sid: u16) {
|
||
// Per spec §"Entry and exit ordering":
|
||
// 1. ExitBegan 2. cancel durings 3. cancel timers
|
||
// 4. exit actions 5. Exited
|
||
#observe_exit_began
|
||
#observe_durings_cancelled
|
||
for &tid in __owned_timers(sid) {
|
||
#observe_timer_cancelled
|
||
}
|
||
#timer_cancel_state_call
|
||
let exits = Self::exits_of(sid);
|
||
for &aid in exits {
|
||
#observe_action_exit
|
||
self.run_action(aid, None);
|
||
if self.terminated { return; }
|
||
}
|
||
#observe_exited
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
#[allow(unused_variables)]
|
||
async fn exit_state(&mut self, sid: u16) {
|
||
#observe_exit_began
|
||
#observe_durings_cancelled
|
||
for &tid in __owned_timers(sid) {
|
||
#observe_timer_cancelled
|
||
}
|
||
#timer_cancel_state_call
|
||
let exits = Self::exits_of(sid);
|
||
for &aid in exits {
|
||
#observe_action_exit
|
||
self.run_action(aid, None).await;
|
||
if self.terminated { return; }
|
||
}
|
||
#observe_exited
|
||
}
|
||
|
||
#[inline]
|
||
fn entries_of(sid: u16) -> &'static [u16] {
|
||
match sid {
|
||
#(#entries_of_arms)*
|
||
_ => &[],
|
||
}
|
||
}
|
||
#[inline]
|
||
fn exits_of(sid: u16) -> &'static [u16] {
|
||
match sid {
|
||
#(#exits_of_arms)*
|
||
_ => &[],
|
||
}
|
||
}
|
||
#[doc(hidden)]
|
||
#[allow(unused_variables)]
|
||
fn __durings_count(sid: u16) -> u16 {
|
||
match sid {
|
||
#(#durings_count_arms)*
|
||
_ => 0,
|
||
}
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn do_terminate(&mut self) {
|
||
if let Some(cur) = self.current {
|
||
let (s, e) = __TERMINATE_RANGE[cur as usize];
|
||
let path = &__TERMINATE_DATA[s as usize..e as usize];
|
||
for &sid in path {
|
||
self.exit_state(sid);
|
||
}
|
||
}
|
||
self.terminated = true;
|
||
self.current = None;
|
||
self.queue.clear();
|
||
#observe_terminated
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn do_terminate(&mut self) {
|
||
if let Some(cur) = self.current {
|
||
let (s, e) = __TERMINATE_RANGE[cur as usize];
|
||
let path = &__TERMINATE_DATA[s as usize..e as usize];
|
||
for &sid in path {
|
||
self.exit_state(sid).await;
|
||
}
|
||
}
|
||
self.terminated = true;
|
||
self.current = None;
|
||
self.queue.clear();
|
||
#observe_terminated
|
||
}
|
||
|
||
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
||
fn dispatch_trigger(&mut self, state: u16, kind: u8, trig: u16) {
|
||
if let Some((actions, target)) = __handler_lookup(state, kind, trig) {
|
||
// Timer-driven dispatch: no event payload, reason = timer.
|
||
self.run_handlers(state, actions, target, None, 2u8, state, trig);
|
||
}
|
||
}
|
||
|
||
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
||
async fn dispatch_trigger(&mut self, state: u16, kind: u8, trig: u16) {
|
||
if let Some((actions, target)) = __handler_lookup(state, kind, trig) {
|
||
self.run_handlers(state, actions, target, None, 2u8, state, trig).await;
|
||
}
|
||
}
|
||
}
|
||
|
||
#default_sender_impl
|
||
#tokio_sender_impl
|
||
#embassy_sender_impl
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
fn _unused(_: Span, _: &Ident) {}
|