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}