//! 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_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, Vec, 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 { 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 { 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, Vec) { // 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 = rev.into_iter().rev().collect(); (exit_path, enter_path) }; let mut exit_data: Vec = Vec::new(); let mut enter_data: Vec = 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, Vec<(u16, u16)>) { let n = states.len(); let parent = |s: u16| -> Option { states[s as usize].parent }; let mut data: Vec = 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, Option)> { use std::collections::BTreeMap; /// `event_id → (action_ids, transition_target)` aggregation for one state. type StateHandlers = BTreeMap, Option)>; 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 = vec![StateHandlers::new(); n_states]; for (sid, state) in ir.states.iter().enumerate() { let mut handlers = state.handlers.iter().collect::>(); 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, Option)> = 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)> { 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 = 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, TokenStream) { let mut idents = Vec::with_capacity(chain.len()); let mut stmts: Vec = 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 { 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 { 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 = 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) { 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 { 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 { 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 ":::". 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:` 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:/` 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)* _ => "", } } #[allow(dead_code)] #[doc(hidden)] #[inline] fn __action_name(aid: u16) -> &'static str { match aid { #(#action_name_arms)* _ => "", } } #[allow(dead_code, unreachable_patterns)] #[doc(hidden)] #[inline] fn __event_name(eid: u16) -> &'static str { match eid { #(#event_name_arms)* _ => "", } } #[allow(dead_code, unreachable_patterns)] #[doc(hidden)] #[inline] fn __timer_name(tid: u16) -> &'static str { match tid { #(#timer_name_arms)* _ => "", } } #[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)* _ => "", } } #[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) -> &'static str { match opt { Some(id) => __state_name(id), 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 = 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::>(); let depth_table = ir .states .iter() .map(|s| { let d = s.depth; quote! { #d } }) .collect::>(); let default_child_table = ir .states .iter() .map(|s| match s.default_child { Some(c) => quote! { Some(#c) }, None => quote! { None }, }) .collect::>(); // 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 = 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 = 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::>(); // 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::>(); 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; } }) .collect::>(); // 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 => ::#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! { ::#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::>(); 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 => ::#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! { ::#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::>(); 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::>(); 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::>(); 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::>(); let state_handler_arms = ir .states .iter() .map(|s| { use std::collections::BTreeMap; #[derive(Default)] struct Group { action_ids: Vec, transition_target: Option, } let mut groups: BTreeMap<(u8, u16), Group> = BTreeMap::new(); let mut handlers = s.handlers.iter().collect::>(); 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 = #transition_tok; return Some((actions, target)); } } }) .collect::>(); quote! { #sid => { match (trigger_kind, trigger_id) { #(#inner_arms)* _ => {} } } } }) .collect::>(); 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::>(); 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 { 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 { 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 = active_durings_per_innermost_state(ir) .into_iter() .map(|(sid, _)| sid) .collect(); let has_active_during_arms: Vec = 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 #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 #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 { 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 #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() {} __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 #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 #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(ctx: #ctx_ty) -> #machine_name<__N> { <#machine_name<__N>>::with_queue_capacity_internal(ctx) } } #[cfg(feature = "embassy")] impl #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 { 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 { ctx: #ctx_ty, current: Option, 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` when on. __journal: ::hsmc::__private::JournalSink, #tokio_fields #embassy_fields } const __PARENT: [Option; #n_states] = [#(#parent_table),*]; const __DEPTH: [u8; #n_states] = [#(#depth_table),*]; const __DEFAULT_CHILD: [Option; #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); #n_dispatch] = [#(#dispatch_lit),*]; // ── Compile-time table-invariant checks ───────────────────── // // Every `unsafe { ... .get_unchecked(...) }` in this generated // module relies on bounds the codegen establishes when emitting // the static tables above. Codify those bounds as const-eval // assertions: any future codegen change that produces an // out-of-range index trips this at *build* time rather than // making it to runtime where it would be UB. A const block // that needs to evaluate at type-check time is the strongest // check we can express in stable Rust. const _: () = { // __PATH_RANGE invariants: for every (from, to) pair the // emitted (s, m, e) triple slices __PATH_DATA in two // contiguous, non-overlapping ranges (exit then enter). // Required: 0 <= s <= m <= e <= __PATH_DATA.len(). All // path entries are themselves valid state ids in // 0..#n_states, but enforcing that here would require // unwrapping every entry; it is enforced indirectly via // the per-id checks below and the codegen test suite. let mut i = 0usize; while i < __PATH_RANGE.len() { let (s, m, e) = __PATH_RANGE[i]; assert!(s as usize <= m as usize, "__PATH_RANGE: s > m"); assert!(m as usize <= e as usize, "__PATH_RANGE: m > e"); assert!( e as usize <= __PATH_DATA.len(), "__PATH_RANGE: e past __PATH_DATA end" ); i += 1; } // __TERMINATE_RANGE invariants: same shape, one (s, e) per // state — the full exit chain up to and including root. let mut i = 0usize; while i < __TERMINATE_RANGE.len() { let (s, e) = __TERMINATE_RANGE[i]; assert!(s as usize <= e as usize, "__TERMINATE_RANGE: s > e"); assert!( e as usize <= __TERMINATE_DATA.len(), "__TERMINATE_RANGE: e past __TERMINATE_DATA end" ); i += 1; } // __DEFAULT_CHILD invariants: every Some(t) names a state // id < N_STATES. None is the no-default sentinel. let mut i = 0usize; while i < __DEFAULT_CHILD.len() { if let Some(t) = __DEFAULT_CHILD[i] { assert!( (t as usize) < __DEFAULT_CHILD.len(), "__DEFAULT_CHILD: target id past N_STATES" ); } i += 1; } // __PATH_DATA itself: every state id stored is < N_STATES. let mut i = 0usize; while i < __PATH_DATA.len() { assert!( (__PATH_DATA[i] as usize) < __DEFAULT_CHILD.len(), "__PATH_DATA: state id past N_STATES" ); i += 1; } // __TERMINATE_DATA: same. let mut i = 0usize; while i < __TERMINATE_DATA.len() { assert!( (__TERMINATE_DATA[i] as usize) < __DEFAULT_CHILD.len(), "__TERMINATE_DATA: state id past N_STATES" ); i += 1; } }; #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)> { 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(ctx: #ctx_ty) -> #machine_name<__N> { <#machine_name<__N>>::with_queue_capacity_internal(ctx) } } impl #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); // SAFETY: __cur is set only by codegen-internal // writes (apply_path/enter_state_no_descent) to // values < #n_states. __ev_id was checked != u16::MAX // and __event_to_id returns < #n_events otherwise. // Hence __dispatch_idx < #n_dispatch = __DISPATCH.len(). // The debug_assert is the safety obligation // expressed at runtime in debug builds — every // hsmc behavior test exercises this site, so a // codegen invariant break trips it loudly. debug_assert!(__dispatch_idx < __DISPATCH.len()); let (__handler_state, __actions, __target) = unsafe { *__DISPATCH.get_unchecked(__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); // SAFETY: see sync branch — same invariant. let (__handler_state, __actions, __target) = unsafe { *__DISPATCH.get_unchecked(__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, 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, 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: &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: &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); // SAFETY: from and target are state ids produced by codegen // (current state, __DISPATCH/__DEFAULT_CHILD targets, root=0). // All are < #n_states, so idx < #n_pairs = __PATH_RANGE.len(). // The triple (s, m, e) was emitted at codegen with the // invariant 0 <= s <= m <= e <= __PATH_DATA.len(), so the // two slice ranges are in-bounds. debug_assert!(idx < __PATH_RANGE.len()); let (s, m, e) = unsafe { *__PATH_RANGE.get_unchecked(idx) }; debug_assert!((s as usize) <= (m as usize) && (m as usize) <= (e as usize) && (e as usize) <= __PATH_DATA.len()); let exit_path = unsafe { __PATH_DATA.get_unchecked(s as usize..m as usize) }; let enter_path = unsafe { __PATH_DATA.get_unchecked(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); // SAFETY: see sync branch — same invariant. let (s, m, e) = unsafe { *__PATH_RANGE.get_unchecked(idx) }; let exit_path = unsafe { __PATH_DATA.get_unchecked(s as usize..m as usize) }; let enter_path = unsafe { __PATH_DATA.get_unchecked(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 { // SAFETY: cur is a state id < #n_states (codegen // invariant — see __DISPATCH lookup safety note). debug_assert!((cur as usize) < __DEFAULT_CHILD.len()); let target = match unsafe { *__DEFAULT_CHILD.get_unchecked(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 { // SAFETY: see sync branch — same invariant. debug_assert!((cur as usize) < __DEFAULT_CHILD.len()); let target = match unsafe { *__DEFAULT_CHILD.get_unchecked(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 { // SAFETY: cur < #n_states (codegen invariant). The // (s, e) tuple was emitted with 0 <= s <= e <= len. debug_assert!((cur as usize) < __TERMINATE_RANGE.len()); let (s, e) = unsafe { *__TERMINATE_RANGE.get_unchecked(cur as usize) }; debug_assert!((s as usize) <= (e as usize) && (e as usize) <= __TERMINATE_DATA.len()); let path = unsafe { __TERMINATE_DATA.get_unchecked(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 { // SAFETY: see sync branch — same invariant. debug_assert!((cur as usize) < __TERMINATE_RANGE.len()); let (s, e) = unsafe { *__TERMINATE_RANGE.get_unchecked(cur as usize) }; debug_assert!((s as usize) <= (e as usize) && (e as usize) <= __TERMINATE_DATA.len()); let path = unsafe { __TERMINATE_DATA.get_unchecked(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) {}