13 Commits
Author SHA1 Message Date
swaits cb38d936dc chore: repoint URLs at git.swaits.com and bump for release
Rewrites remaining github.com / git.sr.ht references to git.swaits.com,
translating GitHub URL shapes to Gitea's (/blob/<ref>/ -> /src/branch/<ref>/,
/tree/<tag> -> /src/tag/<tag>, /actions/workflows/X -> /actions?workflow=X,
/edit/ -> /_edit/).

Left pointing at GitHub on purpose: pull-request links (PRs were not migrated)
and GitHub-only surfaces Gitea lacks (security advisories, discussions).
2026-08-31 17:58:32 -06:00
swaits 5f933f98fa chore(release): 0.6.0 2026-05-04 15:29:24 -06:00
swaits 915b7ce101 chore(bench): migrate dispatch harness from iai-callgrind to gungraun
iai-callgrind is unmaintained; gungraun is the active fork. API is
drop-in (use iai_callgrind → use gungraun, same library_benchmark /
library_benchmark_group / main! macros), so the dispatch.rs body only
needed the import rename.

Also:
- mise.toml: add cargo:gungraun-runner dep so the valgrind harness
  binary tracks the lib version automatically.
- baseline.txt: rebaselined under gungraun_initial. Absolute numbers
  shifted up because gungraun's bench wrapper instruments more than
  iai-callgrind 0.16's did; comparable benches (terminate_event 69→68,
  h8_cross_tree 93→97) confirm HSM dispatch work itself is unchanged.
  Old iai-callgrind named baselines kept as historical context only.
2026-05-04 15:29:23 -06:00
swaits 1f98a9bf5b chore(release): 0.5.1 2026-04-29 09:50:36 -06:00
swaits 9eb1d10b28 chore(release): 0.5.0 2026-04-28 21:03:05 -06:00
swaitsandClaude Opus 4.7 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>
2026-04-28 19:11:29 -06:00
swaitsandClaude Opus 4.7 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>
2026-04-28 16:58:53 -06:00
swaits 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.
2026-04-28 15:24:14 -06:00
swaits 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.
2026-04-27 21:32:22 -06:00
swaits 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.
2026-04-27 21:22:44 -06:00
swaits 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.
2026-04-27 20:15:05 -06:00
swaits 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.
2026-04-22 18:22:04 -06:00
swaits 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
2026-04-16 13:03:03 -06:00