From 98352b0e4d08e6cc72c83d31d4046b2a40f78b64 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 14:04:48 -0600 Subject: [PATCH] safety(codegen): compile-time const-eval checks for table invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every codegen-emitted statechart now contains a const block that verifies the safety preconditions of every unsafe get_unchecked at build time. Any future codegen change that produces an out-of-range offset or a malformed (s, m, e) triple in __PATH_RANGE / __TERMINATE_RANGE fails `cargo check` with a named diagnostic instead of being deferred to runtime UB. Checks performed (per chart): - __PATH_RANGE[i] = (s, m, e) satisfies 0 <= s <= m <= e and e <= __PATH_DATA.len() - __TERMINATE_RANGE[i] = (s, e) satisfies 0 <= s <= e and e <= __TERMINATE_DATA.len() - Every state id stored in __PATH_DATA / __TERMINATE_DATA is < N_STATES - Every Some(t) in __DEFAULT_CHILD names a state id < N_STATES The const block evaluates at type-check (cargo check), produces no runtime code, and runs once per chart definition. Verified by manually injecting a bad triple — rustc reports `E0080: evaluation panicked: __PATH_RANGE: e past __PATH_DATA end`. Combined with the runtime debug_assert! companions (this commit chain) and miri (zero UB on 66 tests), the static and dynamic checks together cover every safety predicate the unsafe blocks rely on. --- hsmc-macros/src/codegen.rs | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/hsmc-macros/src/codegen.rs b/hsmc-macros/src/codegen.rs index 2c0a957..180badb 100644 --- a/hsmc-macros/src/codegen.rs +++ b/hsmc-macros/src/codegen.rs @@ -2462,6 +2462,84 @@ pub fn generate(ir: &Ir) -> TokenStream { 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)]