From b9645f80cb98315837eb74522ab19de402938d7a Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 13:56:03 -0600 Subject: [PATCH] safety(codegen): debug_assert companion for every unsafe get_unchecked Pair every codegen-emitted `unsafe { ... get_unchecked ... }` with a debug_assert! that re-states the safety obligation as a runtime predicate. Free in release (debug_assert! compiles to nothing); in dev/test/miri builds, every dispatch through the 68 behavior tests exercises these checks, so any future regression in the codegen's invariants trips loudly. Sites covered: - step()'s __DISPATCH lookup (sync + async) - apply_path()'s __PATH_RANGE + __PATH_DATA[..] slice ranges (sync + async) - follow_defaults()'s __DEFAULT_CHILD lookup (sync + async) - do_terminate()'s __TERMINATE_RANGE + __TERMINATE_DATA[..] slice ranges (sync + async) Verification: - cargo nextest: 68/68 pass (debug_assert! active) - cargo +nightly miri test: 66/66 pass (UB detector) - cargo bench --bench dispatch: zero release-mode regression (asserts compile out as expected) --- hsmc-macros/src/codegen.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hsmc-macros/src/codegen.rs b/hsmc-macros/src/codegen.rs index d9916b7..2c0a957 100644 --- a/hsmc-macros/src/codegen.rs +++ b/hsmc-macros/src/codegen.rs @@ -2691,6 +2691,11 @@ pub fn generate(ir: &Ir) -> TokenStream { // 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) }; @@ -2973,7 +2978,9 @@ pub fn generate(ir: &Ir) -> TokenStream { // 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) }; @@ -3107,6 +3114,7 @@ pub fn generate(ir: &Ir) -> TokenStream { 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, @@ -3128,6 +3136,7 @@ pub fn generate(ir: &Ir) -> TokenStream { 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, @@ -3208,7 +3217,9 @@ pub fn generate(ir: &Ir) -> TokenStream { 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) }; @@ -3226,7 +3237,9 @@ pub fn generate(ir: &Ir) -> TokenStream { 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) };