From 6a893ffe57a4b7e17421e3668d6768283b13073d Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Wed, 9 Sep 2026 22:05:11 +0200 Subject: [PATCH] fix!: non_exhaustive on ConvergenceReport, and not on the options structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ConvergenceReport` is only ever constructed by `converge` / `converge_partial`, so marking it costs a caller nothing and makes a future field additive. `ConvergenceOptions` and `GameOptions` deliberately stay constructible, against #74's recommendation, because trying it turned up a cost the issue did not anticipate. `Default::default` is not a `const fn`, so `#[non_exhaustive]` + `..Default::default()` — the pattern that makes marking an options struct cheap — does not work in a `const`: error[E0639]: cannot create non-exhaustive struct using struct expression --> tests/competitor_config.rs:13:41 | 13 | const CONVERGENCE: ConvergenceOptions = ConvergenceOptions { `ConvergenceOptions` is `Copy` and a natural const; there is no workaround from outside the crate. That cost is permanent, and adding a field is a one-time major bump. The reasoning is recorded on the type so the next person does not rediscover it. Refs #74. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ --- src/convergence.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/convergence.rs b/src/convergence.rs index 660edd9..c0168cc 100644 --- a/src/convergence.rs +++ b/src/convergence.rs @@ -11,6 +11,14 @@ use smallvec::SmallVec; /// carried by `GameOptions` for a single match scored without a history. The /// defaults are the crate's globals: [`ITERATIONS`](crate::ITERATIONS), /// [`EPSILON`](crate::EPSILON), and undamped EP. +/// +/// Deliberately **not** `#[non_exhaustive]`, unlike [`ConvergenceReport`]. The +/// usual argument for marking an options struct is that `..Default::default()` +/// makes a future field additive — but `Default::default` is not a `const fn`, +/// so marking it would make +/// `const OPTS: ConvergenceOptions = ConvergenceOptions { .. }` impossible from +/// outside the crate, with no workaround. This type is `Copy` and a natural +/// const; that cost is permanent, and adding a field is a one-time major bump. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ConvergenceOptions { /// Hard cap on full forward+backward sweeps. @@ -89,7 +97,12 @@ impl Default for ConvergenceOptions { /// [`InferenceError::NotConverged`](crate::InferenceError::NotConverged) there. /// From [`History::converge_partial`](crate::History::converge_partial) it may /// not be, and `converged` is what says so. +/// Constructed only by `converge` / `converge_partial`, never by a caller, so +/// `#[non_exhaustive]` costs nothing here and lets a future field be additive. +/// The two *options* structs deliberately do not carry it — see the note on +/// [`ConvergenceOptions`]. #[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] pub struct ConvergenceReport { /// Full forward+backward sweeps actually run. `0` for a history with no /// time slices, which is converged trivially.