retroglyph_core/dev.rs
1//! Build-mode vocabulary: which diagnostics a build compiles in.
2//!
3//! retroglyph emits diagnostics that exist purely to shorten the debugging loop: a warning that
4//! a sprite is bigger than the cells reserved for it, a warning that a tint was set on a cell
5//! that resolved to a font glyph rather than a sprite. Each one costs something to produce (a
6//! formatted message, and usually a side table so a 60fps redraw loop reports each offender once
7//! instead of every frame), and none of it is worth anything in a shipped game, where nobody is
8//! reading the log.
9//!
10//! [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) names which kind of build this is, and [`dev_only!`] gates a block on
11//! it. In a release build the const is `false`, the branch folds away, and everything inside it
12//! (message strings, the bookkeeping that dedupes them) is dropped as dead code.
13//!
14//! ```
15//! use retroglyph_core::dev_only;
16//!
17//! # fn report(_: &str, _: usize) {}
18//! # let cache_misses = 3;
19//! dev_only!({
20//! if cache_misses > 0 {
21//! // Costs nothing in a release build: neither the check nor the message survives.
22//! report("glyphs missed the sprite cache", cache_misses);
23//! }
24//! });
25//! ```
26//!
27//! # Two modes, not three
28//!
29//! Engines that own their whole toolchain usually expose three build modes. Flutter's
30//! `debug`/`profile`/`release` is the clearest version: `debug` is unoptimized with every
31//! assertion live, `profile` is optimized but keeps enough instrumentation to attribute a frame
32//! budget, and `release` is what ships.
33//!
34//! Cargo has no `profile` mode in that sense. A profiling build is a release build that keeps
35//! debug symbols (`[profile.profiling] inherits = "release"`, plus `debug = true`), and it is
36//! *supposed* to be one: measuring a build whose diagnostics differ from the shipped build
37//! measures the wrong program. So there are two modes here, and a profiling build resolves to
38//! [`Release`](crate::dev::BuildMode::Release).
39//!
40//! That is also why the gate is written as "is this a dev build" rather than "is this not a
41//! release build". Flutter's own guidance on its `kReleaseMode` constant is to prefer `kDebugMode`
42//! or `assert` precisely because gating on *not release* is what makes a profile build behave
43//! unlike the release build it is meant to predict.
44//!
45//! # How a mode is chosen
46//!
47//! | Build | [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) |
48//! | --- | --- |
49//! | `cargo build`, `cargo test`, `cargo run` | [`Dev`](crate::dev::BuildMode::Dev) |
50//! | `cargo build --release` | [`Release`](crate::dev::BuildMode::Release) |
51//! | a profiling profile inheriting `release` | [`Release`](crate::dev::BuildMode::Release) |
52//! | any build with the `dev` feature on | [`Dev`](crate::dev::BuildMode::Dev) |
53//! | any build with `-C debug-assertions=on` | [`Dev`](crate::dev::BuildMode::Dev) |
54//!
55//! The default signal is `debug_assertions`, which Cargo turns on for the `dev` profile and off
56//! for `release`. It follows whichever profile the consumer built with, so a game gets
57//! diagnostics from `cargo run` and none from `cargo run --release` without configuring anything.
58//!
59//! The `dev` feature forces [`Dev`](crate::dev::BuildMode::Dev) on regardless, for an optimized build that
60//! still reports. This is the equivalent of Unity's "Development Build" checkbox or Bevy's `dev`
61//! feature: release codegen, because an unoptimized build of a renderer is too slow to reproduce
62//! anything frame-dependent, but with the instrumentation left in.
63//!
64//! # Turning diagnostics off in a dev build
65//!
66//! There is no feature for this. Cargo features are additive, so a `no-dev` feature
67//! would be silently defeated by any other crate in the graph that wanted diagnostics.
68//!
69//! Every diagnostic in this workspace goes through the `log` crate, so the two working controls
70//! are the consumer's own log filter at runtime, and `log`'s `max_level_*` /
71//! `release_max_level_*` features, which drop the calls at compile time. Those cut deeper than
72//! this module does: they apply to every `log` user in the graph, not just retroglyph.
73//!
74//! # Load-time versus per-frame
75//!
76//! Not every `log::warn!` in this workspace goes through [`dev_only!`]. The rule is where the
77//! call sits, not what category of mistake it reports: a diagnostic reachable from a redraw loop
78//! is [`dev_only!`]-gated, because at 60fps an ungated one reformats its message and grows its
79//! `seen` dedup table every frame the condition holds. A diagnostic reachable only from a one-time
80//! setup path, such as decoding a tileset, has neither cost to save by gating it, and it may be
81//! reporting an asset or config mistake a consumer wants to see even in a shipped build. So it
82//! stays ungated. `retroglyph-window`'s tileset codepoint-collision warning is the example: it
83//! fires at most once per tileset load, not once per frame.
84
85/// Which diagnostics this build compiles in.
86///
87/// Read [`CURRENT`](Self::CURRENT) for this build's mode, or use [`dev_only!`](crate::dev_only)
88/// to gate a block on it. See the [module docs](self) for how a mode is chosen and why there are
89/// two of them rather than three.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum BuildMode {
92 /// Development diagnostics are compiled in.
93 ///
94 /// Selected by `debug_assertions` (so: any `cargo` command that has not been pointed at the
95 /// `release` profile) or by this crate's `dev` feature.
96 Dev,
97 /// Development diagnostics are compiled out.
98 ///
99 /// Selected by the `release` profile, and by any profile inheriting it, which includes a
100 /// profiling build. Enable the `dev` feature to get an optimized build that still reports.
101 Release,
102}
103
104impl BuildMode {
105 /// The mode this build was compiled in.
106 ///
107 /// A `const`, so a branch on it folds away and the untaken side is dropped as dead code.
108 pub const CURRENT: Self = if cfg!(debug_assertions) || cfg!(feature = "dev") {
109 Self::Dev
110 } else {
111 Self::Release
112 };
113
114 /// Whether this is [`Dev`](Self::Dev).
115 #[must_use]
116 pub const fn is_dev(self) -> bool {
117 matches!(self, Self::Dev)
118 }
119
120 /// Whether this is [`Release`](Self::Release).
121 #[must_use]
122 pub const fn is_release(self) -> bool {
123 matches!(self, Self::Release)
124 }
125}
126
127/// Whether this build compiles in development diagnostics: [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) as a `bool`.
128///
129/// Prefer [`dev_only!`](crate::dev_only) for gating a block. Reach for this constant directly
130/// when the shape of the code makes a macro awkward, such as an early return or a struct field
131/// that only one mode populates.
132pub const DEV: bool = BuildMode::CURRENT.is_dev();
133
134/// Runs `body` only in a build that compiles in development diagnostics.
135///
136/// Expands to `if DEV { body }`. Because [`DEV`](crate::dev::DEV) is a `const`, a release build folds the branch
137/// away and drops `body` with it, including any message strings and bookkeeping it alone
138/// references.
139///
140/// `body` is type-checked in every mode. That is the point: a diagnostic that only compiles on
141/// one profile rots, and the rot surfaces as a broken release build. The cost is that `body` may
142/// not reference items that themselves exist only in a dev build.
143///
144/// Control flow escapes the block on one profile only. A `return`, `?`, `break`, or `continue`
145/// inside `body` runs in a dev build and is skipped entirely in a release build, so the
146/// surrounding function must be correct when the block does nothing. Confine `body` to
147/// diagnostics and their bookkeeping; if the enclosing function's result depends on it, the
148/// profiles disagree.
149///
150/// # Examples
151///
152/// ```
153/// use retroglyph_core::dev_only;
154///
155/// # fn warn_overflow(_: (u32, u32), _: (u32, u32)) {}
156/// let sprite_px = (32, 32);
157/// let cell_px = (16, 16);
158///
159/// dev_only!({
160/// if sprite_px > cell_px {
161/// warn_overflow(sprite_px, cell_px);
162/// }
163/// });
164/// ```
165///
166/// The block form is not required; any statements work.
167///
168/// ```
169/// # use retroglyph_core::dev_only;
170/// # let mut misses = 0;
171/// dev_only!(misses += 1;);
172/// ```
173#[macro_export]
174macro_rules! dev_only {
175 ($($body:tt)*) => {
176 if $crate::dev::DEV {
177 $($body)*
178 }
179 };
180}
181
182#[cfg(test)]
183mod tests {
184 use super::{BuildMode, DEV};
185
186 #[test]
187 fn current_matches_dev_const() {
188 assert_eq!(BuildMode::CURRENT.is_dev(), DEV);
189 }
190
191 #[test]
192 fn is_dev_discriminates_variants() {
193 assert!(BuildMode::Dev.is_dev());
194 assert!(!BuildMode::Release.is_dev());
195 }
196
197 #[test]
198 fn is_release_discriminates_variants() {
199 assert!(BuildMode::Release.is_release());
200 assert!(!BuildMode::Dev.is_release());
201 }
202
203 // Tests build with `debug_assertions` on unless someone deliberately runs them under a
204 // release profile, in which case the `dev` feature is what keeps this true.
205 #[test]
206 fn tests_run_in_a_reporting_build() {
207 assert_eq!(
208 DEV,
209 cfg!(debug_assertions) || cfg!(feature = "dev"),
210 "BuildMode::CURRENT should track debug_assertions and the `dev` feature"
211 );
212 }
213
214 #[test]
215 fn dev_only_body_runs_iff_dev() {
216 let mut ran = false;
217 dev_only!({
218 ran = true;
219 });
220 assert_eq!(ran, DEV);
221 }
222
223 #[test]
224 fn dev_only_accepts_bare_statements() {
225 let mut n = 0;
226 dev_only!(n += 1;);
227 assert_eq!(n, i32::from(DEV));
228 }
229
230 /// Mirrors the `warn_sprite_needs_span`-style shape: a `dev_only!` body that returns early.
231 /// Pins that the early return only happens in a dev build, and that a release build falls
232 /// through to the caller's own trailing value instead.
233 fn returns_early_in_dev() -> bool {
234 dev_only!({
235 return true;
236 });
237 false
238 }
239
240 #[test]
241 fn dev_only_early_return_runs_iff_dev() {
242 assert_eq!(returns_early_in_dev(), DEV);
243 }
244}