From c06200000050cf274e0498c1072183fec80f5fbc Mon Sep 17 00:00:00 2001 From: Carl Sverre <82591+carlsverre@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:42:02 +0000 Subject: [PATCH] Type-check expectation arguments when precept is disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the `enabled` feature is off, the stubs in `macros_stubs.rs` expanded to `{}`, discarding their condition and details expressions entirely. Two problems followed: 1. The arguments were never type-checked in the disabled build, so a typo or type error in an `expect_*!` / `observe!` block only surfaced when the crate was compiled with `enabled` — defeating the "same source compiles identically in every configuration" guarantee that makes `observe!` blocks safe to leave inline. 2. Any temporary that existed solely to feed an expectation became an unused variable, tripping `unused_variables` / `unused_assignments` in downstream crates built with `-D warnings`. Rewrite each stub to reference its arguments inside a dead `if false { … }` block: fully type- and borrow-checked (dead code is still checked), every captured expression is marked used, and the branch is eliminated before codegen — preserving the crate's zero-runtime-overhead promise (arguments are never evaluated at runtime). Argument evaluation mirrors the enabled path in macros.rs (by value, same moves/borrows). Verified: `cargo test` and `cargo clippy --all-targets -- -D warnings` pass with and without `enabled`; a type error inside a disabled `expect_always!` still fails to compile; and a downstream crate with an `observe!`-only temporary no longer warns under `-D warnings`. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/macros_stubs.rs | 120 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/src/macros_stubs.rs b/src/macros_stubs.rs index 82e0472..7b3d34e 100644 --- a/src/macros_stubs.rs +++ b/src/macros_stubs.rs @@ -1,59 +1,155 @@ -// This file must be kept in sync with macros.rs +// This file must be kept in sync with macros.rs. +// +// When the `enabled` feature is off these macros register no catalog entries +// and perform no dispatch — precept compiles away to nothing. But "nothing" +// must not mean "discard the arguments": the condition and details expressions +// still have to be **type-checked**, and every temporary they reference has to +// count as **used**, so that +// +// 1. the same source compiles identically with and without `enabled` (a typo +// or type error in an expectation is caught in every build, not just the +// instrumented one — the guarantee that makes `observe!` blocks safe to +// leave inline), and +// 2. a temporary that exists only to feed an expectation does not trip +// `unused_variables` / `unused_assignments` in a downstream crate built +// with `-D warnings`. +// +// Each expansion therefore places its arguments inside `if false { … }`. That +// block is fully type- and borrow-checked (dead code is still checked), the +// contained expressions mark their captures as used, and the branch is provably +// unreachable so it is eliminated before codegen — keeping the crate's +// zero-runtime-overhead promise: the arguments are never evaluated at runtime. +// +// `$condition` and `json!($details)` are consumed by value to mirror exactly how +// the enabled path in macros.rs evaluates them (same moves, same borrows). #[doc(hidden)] #[macro_export] macro_rules! define_entry { - ($($ignore:tt)*) => {}; + ($expectation:expr, $property:expr) => { + if false { + let _ = $expectation; + let _ = &$property; + } + }; } #[doc(hidden)] #[macro_export] macro_rules! emit_entry { - ($($ignore:tt)*) => {}; + ($entry:expr, $condition:expr) => { + if false { + let _ = $entry; + let _ = $condition; + } + }; + ($entry:expr, $condition:expr, $($details:tt)+) => { + if false { + let _ = $entry; + let _ = $condition; + let _ = $crate::deps::serde_json::json!($($details)+); + } + }; } #[doc(hidden)] #[macro_export] macro_rules! define_and_emit_entry { - ($($ignore:tt)*) => {}; + ($expectation:expr, $property:expr, $condition:expr) => { + if false { + let _ = $expectation; + let _ = &$property; + let _ = $condition; + } + }; + ($expectation:expr, $property:expr, $condition:expr, $($details:tt)+) => { + if false { + let _ = $expectation; + let _ = &$property; + let _ = $condition; + let _ = $crate::deps::serde_json::json!($($details)+); + } + }; } #[macro_export] macro_rules! emit_event { - ($($ignore:tt)*) => {}; + ($name:expr, $($details:tt)+) => { + if false { + let _ = &$name; + let _ = $crate::deps::serde_json::json!($($details)+); + } + }; } #[macro_export] macro_rules! setup_complete { - ($($ignore:tt)*) => {}; + () => {}; + ($($details:tt)+) => { + if false { + let _ = $crate::deps::serde_json::json!($($details)+); + } + }; } #[macro_export] macro_rules! expect_always { - ($($ignore:tt)*) => {}; + ($condition:expr, $property:expr$(, $($details:tt)+)?) => { + $crate::define_and_emit_entry!( + (), $property, $condition $(, $($details)+)? + ) + }; } #[macro_export] macro_rules! expect_always_or_unreachable { - ($($ignore:tt)*) => {}; + ($condition:expr, $property:expr$(, $($details:tt)+)?) => { + $crate::define_and_emit_entry!( + (), $property, $condition $(, $($details)+)? + ) + }; } #[macro_export] macro_rules! expect_sometimes { - ($($ignore:tt)*) => {}; + ($condition:expr, $property:expr$(, $($details:tt)+)?) => { + $crate::define_and_emit_entry!( + (), $property, $condition $(, $($details)+)? + ) + }; } #[macro_export] macro_rules! expect_reachable { - ($($ignore:tt)*) => {}; + ($property:expr$(, $($details:tt)+)?) => { + $crate::define_and_emit_entry!( + (), $property, true $(, $($details)+)? + ) + }; } #[macro_export] macro_rules! expect_unreachable { - ($($ignore:tt)*) => {}; + ($property:expr$(, $($details:tt)+)?) => { + $crate::define_and_emit_entry!( + (), $property, false $(, $($details)+)? + ) + }; } #[macro_export] macro_rules! sometimes_fault { - ($($ignore:tt)*) => {}; + ($name:expr, $fault:expr) => { + if false { + let _ = &$name; + let _ = $fault; + } + }; + ($name:expr, $fault:expr, $($details:tt)+) => { + if false { + let _ = &$name; + let _ = $fault; + let _ = $crate::deps::serde_json::json!($($details)+); + } + }; }