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}