3fc0d7c57d056a6b41c6e62986b0920d986bf32a
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3fc0d7c57d
|
feat(default)!: model default(...) as a real LCA-aware transition
`default(...)` is now a transition with the same semantics as any
event-driven transition. It fires immediately after the declaring
state's entries finish and its durings start, applying the standard
exit-to-LCA / enter-to-target algorithm. Three behavioral changes
follow from this unification:
* Optional. A state with children is no longer required to declare a
`default(...)`. When omitted, the state itself is the resting
innermost active state; substates are reachable only via explicit
transitions. This is the "microwave Standby" pattern — Standby
has substates like LowPower that you enter only on demand, and
`=> Standby;` lands at Standby itself with no descent.
* Targets anywhere. The default target may be any state in the chart
— a child (the classic descent case), a sibling, an ancestor,
anywhere. `default(<sibling-or-ancestor>)` exits the just-entered
state as part of the default-fire transition; the same rule fires
whether you wrote it for descent, redirection, or up-bounce.
* Cycle-checked at compile time. The default-edge graph (out-degree
≤ 1 per node) is rejected if it contains any cycle, including
`default(self)`. `state A { default(A); }` and `A → B → A` chains
fail to compile with a pinpoint error message naming the cycle.
This guarantees default chains terminate at runtime.
Implementation:
* `validate.rs`: drop the "must be direct child" and "must have
children" restrictions; add `validate_default_graph` (iterative
three-color cycle detection via chain walk).
* `ir.rs`: keep the unresolved default ident through to
`resolve_transitions` so non-child targets resolve against the
global state-name map (the same map transition targets use).
* `codegen.rs`: precomputed `(from, to)` paths no longer fold default
descent into the enter slice — each path is LCA-relative
`(exit, enter)` ending exactly at the explicit target.
`descend_defaults` (a parent-pointer walk) is replaced by
`follow_defaults`, which fires real transitions through
`apply_path` and emits `TransitionFired{Internal}` /
`TransitionComplete` brackets per link. Up-transitions don't
trigger default-following (nothing was freshly entered, so no
state's default fires).
* `active_durings_per_innermost_state`: predicate widened to include
composites without a default (they can now be the innermost active
state, so any durings they declare must be raced when current).
BREAKING CHANGES:
* Journal format. Default-fires now emit `TransitionFired` (with
`reason: TransitionReason::Internal`) and `TransitionComplete`
brackets around their entry sequence. `Internal` was previously
reserved and never emitted; it is now active. Tests that
hand-build expected journal sequences must include these brackets
around every default-fire.
* Validation. `default(<non-child>)` and `default` on a childless
state previously raised compile-time errors. Both are now valid.
Code relying on these as guard rails must adapt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2e55484080
|
chore(release): 0.4.1
Bump workspace version 0.4.0 → 0.4.1. Patch release: dispatch hot-path performance only. No public API changes; every chart that compiled against 0.4.0 compiles unchanged and behaves identically. CHANGELOG.md captures the per-pass writeup under the 0.4.1 heading. Headlines (deepest dispatch in the bench suite): h8_cross_tree -82% instructions / -72% cycles vs pre_lca_fix bubble_top -22% instructions / -6% cycles vs handler walk terminate_event -22% instructions / -23% cycles vs PARENT walk The runtime LCA helper, is_up_transition, the per-call heapless::Vec ancestry build, the runtime PARENT bubble walk, and (on timer-free charts) the entire TimerTable machinery are all gone. Callgrind reports the entire HSM machinery as a single inlined `step` symbol. Bench suite expanded from 7 to 13 benches: bubble_leaf/mid/top quantify handler resolution at varying ancestor depths, timer_fire_dispatch covers the only path that exercises pop_expired's non-empty branch, terminate_event covers do_terminate, and bubble_cross_tree covers the realistic HSM worst case (deep bubble + deep cross-tree transition). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>v0.4.1 |
||
|
|
dc97419161
|
perf: precompute transition/dispatch/terminate paths; shrink TimerEntry to u64
Six codegen-side optimization passes against step() and friends. No user-facing API changes; all 296 tests pass under no-default, tokio, embassy, and tokio+journal. Cumulative on h8_cross_tree (deepest dispatch in the suite): -82% instructions / -72% cycles vs the pre_lca_fix baseline. * __PATH_DATA + __PATH_RANGE: per-(from, to) precomputed exit/enter sequences. Up/self/lateral semantics fold into one indexed lookup; removes lca() and is_up_transition() runtime helpers and the per-call heapless::Vec<u16, 16> ancestry build. Hierarchy depth no longer bounded by Vec capacity. * __DISPATCH[(state, event)]: precomputed handler resolution. The runtime __PARENT bubble walk + per-hop __handler_lookup call in step() collapses to one indexed lookup. Bubble resolution is now O(1) regardless of depth -- bubble_top (4-hop) is within 3 instructions of bubble_leaf (handler on current). __handler_lookup retained for timer dispatch. * __TERMINATE_DATA: per-state precomputed exit chain (root inclusive -- chart-root entry/exit actions are valid). do_terminate() indexes the slice instead of walking __PARENT, and uses Deque::clear() (O(1) in heapless 0.8) instead of pop_front-loop. terminate_event bench: -22.5% instr / -23.4% cycles. * TimerTable empty-skip: pop_expired / min_remaining / decrement / cancel_state each get inlined is_empty() early-out with the non-empty body marked #[cold]. * Codegen-static timer-machinery elide: when chart has no Duration triggers, every TimerTable call is omitted from step() and exit_state(). step() returns None directly. dispatch_trigger and the timer-fired arms remain but are unreachable; LLVM DCEs them. * TimerEntry.remaining_ns: u128 -> u64 (~584-year ceiling). Halves entry size; replaces u128 saturating-sub with u64. The smaller machine struct improves cache density on every bench, even timer-free ones. Bench coverage: four new benches added to cover paths the original suite missed -- bubble_leaf/mid/top (handler resolution at varying ancestor depths), timer_fire_dispatch (only bench exercising pop_expired's non-empty branch and dispatch_trigger), terminate_event (only bench exercising do_terminate), bubble_cross_tree (deep bubble + 10-state cross-tree, the realistic HSM worst case). See hsmc/benches/baseline.txt for the per-pass delta blocks and docs/003. latency-analysis.md sections 1, 9, and 12 for the cost-surface analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e633f5dc55
|
perf: O(H) lca() depth-walk + dispatch bench scoreboard
The runtime `lca()` helper used a `heapless::Vec<u16,32>` ancestry-set with a linear `contains()` per node — O(H²) and capped at H ≤ 32. The codegened `__DEPTH` table already had everything needed for an O(H) depth-walk with no allocation and no hierarchy ceiling. Switch to that. Also adds an iai-callgrind dispatch bench harness (Linux-only dev-dep) so future optimizations can be measured deterministically by instruction count. Seven benches cover: cold start, lateral H=2/4/8, up, self, and a self-emit chain. Baseline numbers checked in at `hsmc/benches/baseline.txt`. The latency analysis (`docs/003. latency-analysis.md`) is rounded out with event-entry-point and cooperative-driving audits — three new sections documenting the four event injection paths, the run-loop's cooperative shape, and three correctness invariants (sticky overflow flag, biased-select trap, cancel-safety on durings). Bench deltas (cargo bench --bench dispatch -- --baseline=pre_lca_fix): h2_cold_start -1.9% / -0.3% (priming step, no lca() — noise) h2_warm_lateral -22.8% / -22.1% h4_cross_tree -25.9% / -10.6% h4_up +1.0% / +0.4% (up-transition, no lca() — noise) h4_self -21.5% / -11.7% h8_cross_tree -46.2% / -41.0% ← headline: deepest LCA stress chain_self_emit -15.4% / -10.4% Improvement scales with hierarchy depth, exactly as the O(H²) → O(H) algorithmic change would predict. All tests pass under --no-default-features, --features tokio, and --features embassy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4ea7463089
|
feat(observe)!: unify journal and trace into single observation pipeline
The codegen now emits one `__chart_observe!` invocation per observable
atom. The macro fans out at compile time to whichever sinks are enabled:
the in-memory journal (`feature = "journal"`), `defmt` (`trace-defmt`),
`log` (`trace-log`), and/or `tracing` (`trace-tracing`). Trace and
journal share the same vocabulary by construction — they cannot
diverge. With no sink feature on, every site expands to `()` (zero
overhead). Multiple trace backends now compose; previously they were
mutually exclusive.
Trace lines use a logfmt-style format with a `[statechart:Name]` prefix
and `key=value` pairs, machine-parseable and greppable. Twenty verbs
cover every observable atom — entries/exits with begin/end pairs,
actions with kind, durings, transitions with the reason that triggered
them, event lifecycle (received/delivered/dropped/queued/failed),
timers, and termination.
BREAKING CHANGES (chart syntax unchanged; observability surface only):
- `TraceEvent::TransitionFired` now carries `reason: TransitionReason`
showing whether the transition was driven by an event, a timer, or
internal logic.
- New `TraceEvent` variants for phase pairing: `EnterBegan`, `ExitBegan`,
`TransitionComplete`, `EventReceived`.
- Trace prefix changed from `[Name]` to `[statechart:Name]`.
- The chart-level `trace;` keyword is parsed for backwards compatibility
but no longer gates emission.
- `__chart_trace!` / `__chart_journal!` macros removed (doc-hidden).
Verification: 159 integration tests pass, 0 missed mutants, miri 68
tests clean, Creusot 14/14 VCs discharged. Zero overhead confirmed via
`cargo expand` — every observation site expands to `{}` with no sinks
enabled.
v0.4.0
|
||
|
|
a6d6cd2674
|
chore(release): 0.3.0
Bump workspace version 0.2.0 → 0.3.0. CHANGELOG: documents the additions in this release — - `journal` Cargo feature with full TraceEvent enum + machine API - root state targetable by chart name - 113 deterministic-flow tests - verification/ workspace member with mechanical Creusot proofs - docs/002 formal semantics doc - README "How a chart behaves" building-block section - mise + just tooling overhaul (set shell, set script-interpreter) README: integrates the semantics from docs/002 as eight building- block rules that build on each other (active path, default-descent, event bubbling, transitions, up-transition rule, entry/exit ordering, timer lifecycle, emit / during / termination). Adds an explicit "what hsmc deliberately does NOT have" table with what to use instead. New "Verification" section pointing at `just verify`. No behavior changes for v0.1 / v0.2 charts — every existing chart compiles unchanged.v0.3.0 |
||
|
|
e183cdd95e
|
feat(verify): discharge Creusot VCs end-to-end
Make `just verify` actually invoke Why3 + alt-ergo + z3 against every
Pearlite VC, and fix the mutant + miri pipelines along the way.
Verification crate
- Drop the const-generic CAP types for EventQueue / TimerTable.
Mirror with std::vec::Vec, whose creusot-std specs carry the
proof burden. Capacity behavior is exercised by the runtime's
own unit tests + correspondence tests.
- View impls are #[logic(open)] over a pub field so contracts on
push/pop/etc. can see the link to the inner Vec's @ view.
- 14/14 VCs discharge: EventQueue {new, push, pop, is_empty, len,
clear, Default}; TimerTable {new, len, is_empty, Default};
Entry derives.
- `cargo creusot prove --no-cache` always runs the SMT solvers;
`time` shows ~7s CPU on ~1.2s wall-clock = real parallel work.
Tooling
- verification/mise.toml pins nightly-2025-11-13 + rust-src,
rustc-dev, llvm-tools-preview, rustfmt; declares opam, python,
uv, and the project-local .venv. Inherits root mise.toml's just
+ cargo-* tools.
- just verify clones creusot v0.9.0 to ~/.local/share/hsmc-verify/
and runs the canonical ./INSTALL — that's the only way the
bundled prelude-generator + creusot-rustc actually build.
- just verify --no-cache forces SMT re-runs every time.
- Drop bespoke pin/install of cargo-creusot from mise.toml; the
cargo backend goes through cargo-binstall which can't pin a
multi-bin git workspace at a tag.
Mutants — was missing every JournalSink mutant
- Recipe runs with --features tokio,journal so journal_* and det_*
tests (gated on both) actually execute.
- Add clear_journal_empties_the_journal test in journal_smoke.rs
to kill the JournalSink::clear with-() mutant.
- Remove dead JournalSink::len + is_empty (called nowhere; only
existed as mutation-bait).
Result: 0 missed, 42 caught, 211 unviable (proc-macro emitted
code that mutants can't standalone-compile).
Miri — was hitting the trybuild glob isolation issue
- --exclude hsmc-macros (trybuild ui test isn't UB-relevant)
- --exclude hsmc-verification (different pinned toolchain)
- 66 tests pass under Miri, 0 UB.
Gitignore
- /verif/, /why3find.json, /.why3find/, **/.venv/ — all
regenerated by `just verify`. Repo stays clean of per-machine
prover timings + venv artifacts.
Misc
- Fix root-state targetability: `on(Trig) => <ChartName>;` resolves
to root id 0 in both ir.rs::resolve_transitions and
validate.rs's separate name_map.
- `set shell` and `set script-interpreter` route every justfile
recipe through `mise exec`, so all subshells see the right
Rust + opam + cargo-* tools regardless of how `just` was
invoked. No shebangs anywhere.
|
||
|
|
081bd28cca
|
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.
|
||
|
|
4d7baf7236
|
fix!: up-transitions no longer exit and re-enter the target ancestor
A transition whose target is already active (an ancestor of the innermost active state) now unwinds only the subtree strictly below the target. The target's entry/exit actions do not fire and its default() does not re-descend — you cannot re-enter a state you never left. The old behavior could loop infinitely when a child transitioned to its own parent. Self-transitions (target == innermost) and lateral transitions are unchanged. See spec §2.6 and tests §T2.8 / T2.8b / T2.8c. Also in this release: * Full workspace justfile (check, test, coverage, mutants, pre-release) * Unit tests covering all __private internals; `cargo mutants` now exits clean (38 caught, 178 unviable, 0 missed, 0 timeouts). * .config/nextest.toml with slow-timeout/terminate-after so hung tests fail fast rather than wedging cargo-mutants runs. BREAKING CHANGE: transition-to-ancestor semantics. Machines that relied on the old re-entry behavior of up-transitions will observe different exit/entry action sequences.v0.2.0 |
||
|
|
bc7e17e58e
|
chore: prepare 0.1.0 for crates.io publish
Add LICENSE-MIT and LICENSE-APACHE at workspace root and in each member package so the dual MIT OR Apache-2.0 license ships in the published .crate. Fill in publish metadata (authors, repository, homepage, readme, keywords, categories) across the workspace and both members. Add CHANGELOG.md with a 0.1.0 initial release entry. Fix clippy lints: unused enumerate index, collapsible if, Default impls for TimerTable, SimRadio, and Ctx, use RangeInclusive::contains. Exclude examples/microwave_tui.rs from the published package. The file stays in-tree but has known unawaited async log futures that need a dedicated pass before it ships. Apply cargo fmt across the workspace.v0.1.0 |
||
|
|
0341cf9379
|
initial commit: hsmc v0.2 with during: activities
Hierarchical statechart codegen for Rust with: - during: activities for state-scoped concurrent I/O - field-list borrowing (split-borrow inside the run loop) - embassy-first async with tokio support - STATE_CHART const for tooling/inspection - examples: microwave, microwave_tui, embassy_full, during_radio |