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 get(&self) -> bool {
129        self.value
130            .load(Ordering::Relaxed)
131            .unwrap_or_else(|| match self.marker.status() {
132                Status::OptIn => false,
133                Status::OptOut => true,
134                Status::DeprecatedDiscard => false,
135                Status::DeprecatedDefault => false,
136            })
137    }
138
139    /// Sets the state of an experimental option.
140    ///
141    /// # Safety
142    /// This method is unsafe to emphasize that experimental options are not designed to change
143    /// dynamically at runtime.
144    /// Changing their state at arbitrary points can lead to inconsistent behavior.
145    /// You should set experimental options only during initialization, before the application fully
146    /// starts.
147    pub unsafe fn set(&self, value: bool) {
148        self.value.store(value, Ordering::Relaxed);
149    }
150
151    /// Unsets an experimental option, resetting it to an uninitialized state.
152    ///
153    /// # Safety
154    /// Like [`set`](Self::set), this method is unsafe to highlight that experimental options should
155    /// remain stable during runtime.
156    /// Only unset options in controlled, initialization contexts to avoid unpredictable behavior.
157    pub unsafe fn unset(&self) {
158        self.value.store(None, Ordering::Relaxed);
159    }
160}
161
162impl Debug for ExperimentalOption {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        let add_description = f.sign_plus();
165        let mut debug_struct = f.debug_struct("ExperimentalOption");
166        debug_struct.field("identifier", &self.identifier());
167        debug_struct.field("value", &self.get());
168        debug_struct.field("stability", &self.status());
169        if add_description {
170            debug_struct.field("description", &self.description());
171        }
172        debug_struct.finish()
173    }
174}
175
176impl PartialEq for ExperimentalOption {
177    fn eq(&self, other: &Self) -> bool {
178        // if both underlying atomics point to the same value, we talk about the same option
179        self.value.as_ptr() == other.value.as_ptr()
180    }
181}
182
183impl Eq for ExperimentalOption {}
184
185/// Sets the state of all experimental option that aren't deprecated.
186///
187/// # Safety
188/// This method is unsafe to emphasize that experimental options are not designed to change
189/// dynamically at runtime.
190/// Changing their state at arbitrary points can lead to inconsistent behavior.
191/// You should set experimental options only during initialization, before the application fully
192/// starts.
193pub unsafe fn set_all(value: bool) {
194    for option in ALL {
195        match option.status() {
196            // SAFETY: The safety bounds for `ExperimentalOption.set` are the same as this function.
197            Status::OptIn | Status::OptOut => unsafe { option.set(value) },
198            Status::DeprecatedDefault | Status::DeprecatedDiscard => {}
199        }
200    }
201}
202
203pub(crate) trait DynExperimentalOptionMarker {
204    fn identifier(&self) -> &'static str;
205    fn description(&self) -> &'static str;
206    fn status(&self) -> Status;
207}
208
209impl<M: options::ExperimentalOptionMarker> DynExperimentalOptionMarker for M {
210    fn identifier(&self) -> &'static str {
211        M::IDENTIFIER
212    }
213
214    fn description(&self) -> &'static str {
215        M::DESCRIPTION
216    }
217
218    fn status(&self) -> Status {
219        M::STATUS
220    }
221}