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`] 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`](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`] |
48//! | --- | --- |
49//! | `cargo build`, `cargo test`, `cargo run` | [`Dev`](BuildMode::Dev) |
50//! | `cargo build --release` | [`Release`](BuildMode::Release) |
51//! | a profiling profile inheriting `release` | [`Release`](BuildMode::Release) |
52//! | any build with the `dev` feature on | [`Dev`](BuildMode::Dev) |
53//! | any build with `-C debug-assertions=on` | [`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`](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 deliberately 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/// Which diagnostics this build compiles in.
75///
76/// Read [`CURRENT`](Self::CURRENT) for this build's mode, or use [`dev_only!`](crate::dev_only)
77/// to gate a block on it. See the [module docs](self) for how a mode is chosen and why there are
78/// two of them rather than three.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80#[non_exhaustive]
81pub enum BuildMode {
82 /// Development diagnostics are compiled in.
83 ///
84 /// Selected by `debug_assertions` (so: any `cargo` command that has not been pointed at the
85 /// `release` profile) or by this crate's `dev` feature.
86 Dev,
87 /// Development diagnostics are compiled out.
88 ///
89 /// Selected by the `release` profile, and by any profile inheriting it, which includes a
90 /// profiling build. Enable the `dev` feature to get an optimized build that still reports.
91 Release,
92}
93
94impl BuildMode {
95 /// The mode this build was compiled in.
96 ///
97 /// A `const`, so a branch on it folds away and the untaken side is dropped as dead code.
98 pub const CURRENT: Self = if cfg!(debug_assertions) || cfg!(feature = "dev") {
99 Self::Dev
100 } else {
101 Self::Release
102 };
103
104 /// Whether this is [`Dev`](Self::Dev).
105 #[must_use]
106 pub const fn is_dev(self) -> bool {
107 matches!(self, Self::Dev)
108 }
109}
110
111/// Whether this build compiles in development diagnostics: [`BuildMode::CURRENT`] as a `bool`.
112///
113/// Prefer [`dev_only!`](crate::dev_only) for gating a block. Reach for this constant directly
114/// when the shape of the code makes a macro awkward, such as an early return or a struct field
115/// that only one mode populates.
116pub const DEV: bool = BuildMode::CURRENT.is_dev();
117
118/// Runs `body` only in a build that compiles in development diagnostics.
119///
120/// Expands to `if DEV { body }`. Because [`DEV`] is a `const`, a release build folds the branch
121/// away and drops `body` with it, including any message strings and bookkeeping it alone
122/// references.
123///
124/// `body` is type-checked in every mode. That is the point: a diagnostic that only compiles on
125/// one profile rots, and the rot surfaces as a broken release build. The cost is that `body` may
126/// not reference items that themselves exist only in a dev build.
127///
128/// # Examples
129///
130/// ```
131/// use retroglyph_core::dev_only;
132///
133/// # fn warn_overflow(_: (u32, u32), _: (u32, u32)) {}
134/// let sprite_px = (32, 32);
135/// let cell_px = (16, 16);
136///
137/// dev_only!({
138/// if sprite_px > cell_px {
139/// warn_overflow(sprite_px, cell_px);
140/// }
141/// });
142/// ```
143///
144/// The block form is not required; any statements work.
145///
146/// ```
147/// # use retroglyph_core::dev_only;
148/// # let mut misses = 0;
149/// dev_only!(misses += 1;);
150/// ```
151#[macro_export]
152macro_rules! dev_only {
153 ($($body:tt)*) => {
154 if $crate::dev::DEV {
155 $($body)*
156 }
157 };
158}
159
160#[cfg(test)]
161mod tests {
162 use super::{BuildMode, DEV};
163
164 #[test]
165 fn current_matches_dev_const() {
166 assert_eq!(BuildMode::CURRENT.is_dev(), DEV);
167 }
168
169 // Tests build with `debug_assertions` on unless someone deliberately runs them under a
170 // release profile, in which case the `dev` feature is what keeps this true.
171 #[test]
172 fn tests_run_in_a_reporting_build() {
173 assert_eq!(
174 DEV,
175 cfg!(debug_assertions) || cfg!(feature = "dev"),
176 "BuildMode::CURRENT should track debug_assertions and the `dev` feature"
177 );
178 }
179
180 #[test]
181 fn dev_only_body_runs_iff_dev() {
182 let mut ran = false;
183 dev_only!({
184 ran = true;
185 });
186 assert_eq!(ran, DEV);
187 }
188
189 #[test]
190 fn dev_only_accepts_bare_statements() {
191 let mut n = 0;
192 dev_only!(n += 1;);
193 assert_eq!(n, i32::from(DEV));
194 }
195}