journal + determinism tests + Creusot verification scaffolding
Phase 1 — semantics doc
docs/002. hsmc-semantics-formal.md
Phase 2 — deterministic execution journal
hsmc/src/journal.rs (TraceEvent, JournalSink, ActionKind)
hsmc-macros: __chart_journal! emission at every observable site
(entry/exit/action/transition/timer arm-cancel-fire/during/emit/
event delivery/terminate). Per-state during count helpers, stable
chart-hash via FNV-1a. Action observation moved out of run_action
and into call sites so ActionKind is accurate.
Public API on the generated machine: journal(), take_journal(),
clear_journal(), CHART_HASH.
Phase 3 — verification crate (Creusot scaffolding)
verification/{Cargo.toml,mise.toml,requirements.txt,INVARIANTS.md}
verification/src/{event_queue.rs,timer_table.rs} — Pearlite
contracts on simplified mirrors of the runtime types. cargo
creusot generates Coma IR cleanly; VCs not yet discharged.
Correspondence tests drive the verified mirror and the runtime
type with the same op sequences and assert observable parity.
Tests — 113 deterministic-flow tests across 22 files. Each compares a
hand-built expected journal byte-for-byte against actual, anchored
to a spec section: initial entry, lateral/self/up transitions,
bubbling, timers, emit, ordering, durings, step/dispatch
equivalence, root-targeting, current_state, termination.
Tooling — mise.toml (root): stable Rust + cargo subcommands.
verification/mise.toml: pinned nightly-2025-11-13 + opam + python
venv + uv. just verify is fully self-installing.
Bug fix — root state targetable by the chart's own name (e.g.
`on(Trig) => ChartName;`) per spec §"How transitions work".
CHART_HASH — stable FNV-1a fingerprint over the chart's structural
definition; mismatch on replay = different chart.
This commit is contained in:
55 files changed
+6476
-151
No files matched your search
@@ -26,6 +26,11 @@ pub struct Ir {
|
||||
/// Duration triggers referenced anywhere, in first-seen order.
|
||||
/// Each one gets a trigger id used by the timer table.
|
||||
pub duration_triggers: Vec<DurationTrigger>,
|
||||
/// True when the chart declared `trace;` at root. Codegen routes
|
||||
/// trace calls to `::hsmc::__chart_trace!`, which dispatches to a
|
||||
/// backend (defmt / log / tracing / no-op) based on the user's
|
||||
/// `hsmc/trace-*` feature selection in their `Cargo.toml`.
|
||||
pub trace_enabled: bool,
|
||||
}
|
||||
|
||||
pub struct ActionIr {
|
||||
@@ -122,6 +127,7 @@ pub fn build_ir(input: StatechartInput) -> syn::Result<Ir> {
|
||||
}
|
||||
};
|
||||
let terminate_event = root_body.terminate.as_ref().map(|(i, _)| i.clone());
|
||||
let trace_enabled = root_body.trace_enabled;
|
||||
|
||||
let mut ir = Ir {
|
||||
name,
|
||||
@@ -133,6 +139,7 @@ pub fn build_ir(input: StatechartInput) -> syn::Result<Ir> {
|
||||
actions: Vec::new(),
|
||||
event_variants: Vec::new(),
|
||||
duration_triggers: Vec::new(),
|
||||
trace_enabled,
|
||||
};
|
||||
|
||||
// Two passes: collect the set of declared state names first so
|
||||
@@ -140,6 +147,9 @@ pub fn build_ir(input: StatechartInput) -> syn::Result<Ir> {
|
||||
// (transition) or a handler fn (action) based on the declared set.
|
||||
let mut state_names = std::collections::HashSet::new();
|
||||
collect_state_names(&root_body, &mut state_names);
|
||||
// Per spec: the root state is just a state, named after the chart.
|
||||
// `on(...) => <ChartName>;` must classify as a transition to root.
|
||||
state_names.insert(ir.name.to_string());
|
||||
|
||||
let name_span = ir.name.span();
|
||||
let root_name = Ident::new("__Root", name_span);
|
||||
@@ -152,6 +162,13 @@ pub fn build_ir(input: StatechartInput) -> syn::Result<Ir> {
|
||||
);
|
||||
lower_body(&mut ir, root_id, &root_body, true, &state_names)?;
|
||||
|
||||
// Intern the terminate event variant so the journal can record it
|
||||
// by id (otherwise `__event_to_id` returns u16::MAX for the very
|
||||
// event that triggered shutdown, which is useless for replay).
|
||||
if let Some(t_ident) = ir.terminate_event.clone() {
|
||||
let _ = intern_event(&mut ir, &t_ident, None)?;
|
||||
}
|
||||
|
||||
Ok(ir)
|
||||
}
|
||||
|
||||
@@ -495,6 +512,12 @@ pub fn resolve_transitions(ir: &mut Ir) -> syn::Result<()> {
|
||||
name_map.insert(s.name.to_string(), s.id);
|
||||
}
|
||||
}
|
||||
// Per spec: the root state is targetable as a transition destination
|
||||
// by the chart's own name. Find the root (parent is None) and map
|
||||
// `<ChartName>` → root_id.
|
||||
if let Some(root) = ir.states.iter().find(|s| s.parent.is_none()) {
|
||||
name_map.insert(ir.name.to_string(), root.id);
|
||||
}
|
||||
|
||||
let n = ir.states.len();
|
||||
for i in 0..n {
|
||||
|
||||
Reference in new issue
Block a user