nu_experimental/
lib.rs

1//! Experimental Options for the Nu codebase.
2//!
3//! This crate defines all experimental options used in Nushell.
4//!
5//! An [`ExperimentalOption`] is basically a fancy global boolean.
6//! It should be set very early during initialization and lets us switch between old and new
7//! behavior for parts of the system.
8//!
9//! The goal is to have a consistent way to handle experimental flags across the codebase, and to
10//! make it easy to find all available options.
11//!
12//! # Usage
13//!
14//! Using an option is simple:
15//!
16//! ```rust
17//! if nu_experimental::EXAMPLE.get() {
18//!     // new behavior
19//! } else {
20//!     // old behavior
21//! }
22//! ```
23//!
24//! # Adding New Options
25//!
26//! 1. Create a new module in `options.rs`.
27//! 2. Define a marker struct and implement `ExperimentalOptionMarker` for it.
28//! 3. Add a new static using `ExperimentalOption::new`.
29//! 4. Add the static to [`ALL`].
30//!
31//! That's it. See [`EXAMPLE`] in `options/example.rs` for a complete example.
32//!
33//! # For Users
34//!
35//! Users can view enabled options using either `version` or `debug experimental-options`.
36//!
37//! To enable or disable options, use either the `NU_EXPERIMENTAL_OPTIONS` environment variable
38//! (see [`ENV`]), or pass them via CLI using `--experimental-options`, e.g.:
39//!
40//! ```sh
41//! nu --experimental-options=[example]
42//! ```
43//!
44//! # For Embedders
45//!
46//! If you're embedding Nushell, prefer using [`parse_env`] or [`parse_iter`] to load options.
47//!
48//! `parse_iter` is useful if you want to feed in values from other sources.
49//! Since options are expected to stay stable during runtime, make sure to do this early.
50//!
51//! You can also call [`ExperimentalOption::set`] manually, but be careful with that.
52
53use crate::util::AtomicMaybe;
54use std::{fmt::Debug, sync::atomic::Ordering};
55
56mod options;
57mod parse;
58mod util;
59
60pub use options::*;
61pub use parse::*;
62
63/// The status of an experimental option.
64///
65/// An option can either be disabled by default ([`OptIn`](Self::OptIn)) or enabled by default
66/// ([`OptOut`](Self::OptOut)), depending on its expected stability.
67///
68/// Experimental options can be deprecated in two ways:
69/// - If the feature becomes default behavior, it's marked as
70///   [`DeprecatedDefault`](Self::DeprecatedDefault).
71/// - If the feature is being fully removed, it's marked as
72///   [`DeprecatedDiscard`](Self::DeprecatedDiscard) and triggers a warning.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum Status {
75    /// Disabled by default.
76    OptIn,
77    /// Enabled by default.
78    OptOut,
79    /// Deprecated as an experimental option; now default behavior.
80    DeprecatedDefault,
81    /// Deprecated; the feature will be removed and triggers a warning.
82    DeprecatedDiscard,
83}
84
85/// Experimental option (aka feature flag).
86///
87/// This struct holds one experimental option that can change some part of Nushell's behavior.
88/// These options let users opt in or out of experimental changes while keeping the rest stable.
89/// They're useful for testing new ideas and giving users a way to go back to older behavior if needed.
90///
91/// You can find all options in the statics of [`nu_experimental`](crate).
92/// Everything there, except [`ALL`], is a toggleable option.
93/// `ALL` gives a full list and can be used to check which options are set.
94///
95/// The [`Debug`] implementation shows the option's identifier, stability, and current value.
96/// To also include the description in the output, use the
97/// [plus sign](std::fmt::Formatter::sign_plus), e.g. `format!("{OPTION:+#?}")`.
98pub struct ExperimentalOption {
99    value: AtomicMaybe,
100    marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync),
101}
102
103impl ExperimentalOption {
104    /// Construct a new `ExperimentalOption`.
105    ///
106    /// This should only be used to define a single static for a marker.
107    pub(crate) const fn new(
108        marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync),
109    ) -> Self {
110        Self {
111            value: AtomicMaybe::new(None),
112            marker,
113        }
114    }
115
116    pub fn identifier(&self) -> &'static str {
117        self.marker.identifier()
118    }
119
120    pub fn description(&self) -> &'static str {
121        self.marker.description()
122    }
123
124    pub fn status(&self) -> Status {
125        self.marker.status()
126    }
127
128    pub fn since(&self) -> Version {
129        self.marker.since()
130    }
131
132    pub fn issue_id(&self) -> u32 {
133        self.marker.issue()
134    }
135
136    pub fn issue_url(&self) -> String {
137        format!(
138            "https://github.com/nushell/nushell/issues/{}",
139            self.marker.issue()
140        )
141    }
142
143    pub fn get(&self) -> bool {
144        self.value
145            .load(Ordering::Relaxed)
146            .unwrap_or_else(|| match self.marker.status() {
147                Status::OptIn => false,
148                Status::OptOut => true,
149                Status::DeprecatedDiscard => false,
150                Status::DeprecatedDefault => false,
151            })
152    }
153
154    /// Sets the state of an experimental option.
155    ///
156    /// # Safety
157    /// This method is unsafe to emphasize that experimental options are not designed to change
158    /// dynamically at runtime.
159    /// Changing their state at arbitrary points can lead to inconsistent behavior.
160    /// You should set experimental options only during initialization, before the application fully
161    /// starts.
162    pub unsafe fn set(&self, value: bool) {
163        self.value.store(value, Ordering::Relaxed);
164    }
165
166    /// Unsets an experimental option, resetting it to an uninitialized state.
167    ///
168    /// # Safety
169    /// Like [`set`](Self::set), this method is unsafe to highlight that experimental options should
170    /// remain stable during runtime.
171    /// Only unset options in controlled, initialization contexts to avoid unpredictable behavior.
172    pub unsafe fn unset(&self) {
173        self.value.store(None, Ordering::Relaxed);
174    }
175}
176
177impl Debug for ExperimentalOption {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let add_description = f.sign_plus();
180        let mut debug_struct = f.debug_struct("ExperimentalOption");
181        debug_struct.field("identifier", &self.identifier());
182        debug_struct.field("value", &self.get());
183        debug_struct.field("stability", &self.status());
184        if add_description {
185            debug_struct.field("description", &self.description());
186        }
187        debug_struct.finish()
188    }
189}
190
191impl PartialEq for ExperimentalOption {
192    fn eq(&self, other: &Self) -> bool {
193        // if both underlying atomics point to the same value, we talk about the same option
194        self.value.as_ptr() == other.value.as_ptr()
195    }
196}
197
198impl Eq for ExperimentalOption {}
199
200/// Sets the state of all experimental option that aren't deprecated.
201///
202/// # Safety
203/// This method is unsafe to emphasize that experimental options are not designed to change
204/// dynamically at runtime.
205/// Changing their state at arbitrary points can lead to inconsistent behavior.
206/// You should set experimental options only during initialization, before the application fully
207/// starts.
208pub unsafe fn set_all(value: bool) {
209    for option in ALL {
210        match option.status() {
211            // SAFETY: The safety bounds for `ExperimentalOption.set` are the same as this function.
212            Status::OptIn | Status::OptOut => unsafe { option.set(value) },
213            Status::DeprecatedDefault | Status::DeprecatedDiscard => {}
214        }
215    }
216}
217
218pub(crate) trait DynExperimentalOptionMarker {
219    fn identifier(&self) -> &'static str;
220    fn description(&self) -> &'static str;
221    fn status(&self) -> Status;
222    fn since(&self) -> Version;
223    fn issue(&self) -> u32;
224}
225
226impl<M: options::ExperimentalOptionMarker> DynExperimentalOptionMarker for M {
227    fn identifier(&self) -> &'static str {
228        M::IDENTIFIER
229    }
230
231    fn description(&self) -> &'static str {
232        M::DESCRIPTION
233    }
234
235    fn status(&self) -> Status {
236        M::STATUS
237    }
238
239    fn since(&self) -> Version {
240        M::SINCE
241    }
242
243    fn issue(&self) -> u32 {
244        M::ISSUE
245    }
246}