sway_features/
lib.rs

1use std::collections::HashMap;
2
3use clap::{Parser, ValueEnum};
4
5macro_rules! features {
6    ($($name:ident = $enabled:literal, $url:literal),* $(,)?) => {
7        paste::paste! {
8            #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, Hash)]
9            #[value(rename_all = "snake")]
10            pub enum Feature {
11                $(
12                    [<$name:camel>],
13                )*
14            }
15
16            impl Feature {
17                pub const CFG: &[&str] = &[
18                    $(
19                        stringify!([<experimental_ $name:snake>]),
20                    )*
21                ];
22
23                pub fn name(&self) -> &'static str {
24                    match self {
25                        $(
26                            Feature::[<$name:camel>] => {
27                                stringify!([<$name:snake>])
28                            },
29                        )*
30                    }
31                }
32
33                pub fn url(&self) -> &'static str {
34                    match self {
35                        $(
36                            Feature::[<$name:camel>] => {
37                                $url
38                            },
39                        )*
40                    }
41                }
42
43                pub fn error_because_is_disabled(&self, span: &sway_types::Span) -> sway_error::error::CompileError {
44                    match self {
45                        $(
46                            Self::[<$name:camel>] => {
47                                sway_error::error::CompileError::FeatureIsDisabled {
48                                    feature: stringify!([<$name:snake>]).into(),
49                                    url: $url.into(),
50                                    span: span.clone()
51                                }
52                            },
53                        )*
54                    }
55                }
56            }
57
58            impl std::str::FromStr for Feature {
59                type Err = Error;
60
61                fn from_str(s: &str) -> Result<Self, Self::Err> {
62                    match s {
63                        $(
64                            stringify!([<$name:snake>]) => {
65                                Ok(Self::[<$name:camel>])
66                            },
67                        )*
68                        _ => Err(Error::UnknownFeature(s.to_string())),
69                    }
70                }
71            }
72
73            #[derive(Copy, Clone, Debug, PartialEq, Eq)]
74            pub struct ExperimentalFeatures {
75                $(
76                    pub [<$name:snake>]: bool,
77                )*
78            }
79
80            impl std::default::Default for ExperimentalFeatures {
81                fn default() -> Self {
82                    Self {
83                        $(
84                            [<$name:snake>]: $enabled,
85                        )*
86                    }
87                }
88            }
89
90            impl ExperimentalFeatures {
91                pub fn set_enabled_by_name(&mut self, feature: &str, enabled: bool) -> Result<(), Error> {
92                    let feature = feature.trim();
93                    match feature {
94                        $(
95                            stringify!([<$name:snake>]) => {
96                                self.[<$name:snake>] = enabled;
97                                Ok(())
98                            },
99                        )*
100                        "" => Ok(()),
101                        _ => Err(Error::UnknownFeature(feature.to_string())),
102                    }
103                }
104
105                pub fn set_enabled(&mut self, feature: Feature, enabled: bool) {
106                    match feature {
107                        $(
108                            Feature::[<$name:camel>] => {
109                                self.[<$name:snake>] = enabled
110                            },
111                        )*
112                    }
113                }
114
115                /// Used for testing if a `#[cfg(...)]` feature is enabled.
116                /// Already prepends "experimental_" to the feature name.
117                pub fn is_enabled_for_cfg(&self, cfg: &str) -> Result<bool, Error> {
118                    match cfg {
119                        $(
120                            stringify!([<experimental_ $name:snake>]) => Ok(self.[<$name:snake>]),
121                        )*
122                        _ => Err(Error::UnknownFeature(cfg.to_string()))
123                    }
124                }
125
126                $(
127                pub fn [<with_ $name:snake>](mut self, enabled: bool) -> Self {
128                    self.[<$name:snake>] = enabled;
129                    self
130                }
131                )*
132            }
133        }
134    };
135}
136
137impl ExperimentalFeatures {
138    /// Experimental features will be applied in the following order:
139    /// 1 - manifest (no specific order)
140    /// 2 - cli_no_experimental
141    /// 3 - cli_experimental
142    /// 4 - FORC_NO_EXPERIMENTAL (env var)
143    /// 5 - FORC_EXPERIMENTAL (env var)
144    pub fn new(
145        manifest: &HashMap<String, bool>,
146        cli_experimental: &[Feature],
147        cli_no_experimental: &[Feature],
148    ) -> Result<ExperimentalFeatures, Error> {
149        let mut experimental = ExperimentalFeatures::default();
150
151        experimental.parse_from_package_manifest(manifest)?;
152
153        for f in cli_no_experimental {
154            experimental.set_enabled(*f, false);
155        }
156
157        for f in cli_experimental {
158            experimental.set_enabled(*f, true);
159        }
160
161        experimental.parse_from_environment_variables()?;
162
163        Ok(experimental)
164    }
165}
166
167features! {
168    new_encoding = true,
169    "https://github.com/FuelLabs/sway/issues/5727",
170    references = true,
171    "https://github.com/FuelLabs/sway/issues/5063",
172    const_generics = false,
173    "https://github.com/FuelLabs/sway/issues/6860",
174    new_hashing = true,
175    "https://github.com/FuelLabs/sway/issues/7256",
176}
177
178#[derive(Clone, Debug, Default, Parser)]
179pub struct CliFields {
180    /// Comma separated list of all experimental features that will be enabled
181    #[clap(long, value_delimiter = ',')]
182    pub experimental: Vec<Feature>,
183
184    /// Comma separated list of all experimental features that will be disabled
185    #[clap(long, value_delimiter = ',')]
186    pub no_experimental: Vec<Feature>,
187}
188
189impl CliFields {
190    pub fn experimental_as_cli_string(&self) -> Option<String> {
191        Self::features_as_cli_string(&self.experimental)
192    }
193
194    pub fn no_experimental_as_cli_string(&self) -> Option<String> {
195        Self::features_as_cli_string(&self.no_experimental)
196    }
197
198    fn features_as_cli_string(features: &[Feature]) -> Option<String> {
199        if features.is_empty() {
200            None
201        } else {
202            Some(
203                features
204                    .iter()
205                    .map(|f| f.name())
206                    .collect::<Vec<_>>()
207                    .join(","),
208            )
209        }
210    }
211}
212
213#[derive(Debug)]
214pub enum Error {
215    ParseError(String),
216    UnknownFeature(String),
217}
218
219impl std::fmt::Display for Error {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Error::ParseError(feature) => f.write_fmt(format_args!(
223                "Experimental feature \"{feature}\" cannot be parsed."
224            )),
225            Error::UnknownFeature(feature) => {
226                f.write_fmt(format_args!("Unknown experimental feature: \"{feature}\"."))
227            }
228        }
229    }
230}
231
232impl ExperimentalFeatures {
233    #[allow(clippy::iter_over_hash_type)]
234    pub fn parse_from_package_manifest(
235        &mut self,
236        experimental: &std::collections::HashMap<String, bool>,
237    ) -> Result<(), Error> {
238        for (feature, enabled) in experimental {
239            self.set_enabled_by_name(feature, *enabled)?;
240        }
241        Ok(())
242    }
243
244    /// Enable and disable features using comma separated feature names from
245    /// environment variables "FORC_EXPERIMENTAL" and "FORC_NO_EXPERIMENTAL".
246    pub fn parse_from_environment_variables(&mut self) -> Result<(), Error> {
247        if let Ok(features) = std::env::var("FORC_NO_EXPERIMENTAL") {
248            self.parse_comma_separated_list(&features, false)?;
249        }
250
251        if let Ok(features) = std::env::var("FORC_EXPERIMENTAL") {
252            self.parse_comma_separated_list(&features, true)?;
253        }
254
255        Ok(())
256    }
257
258    pub fn parse_comma_separated_list(
259        &mut self,
260        features: impl AsRef<str>,
261        enabled: bool,
262    ) -> Result<(), Error> {
263        for feature in features.as_ref().split(',') {
264            self.set_enabled_by_name(feature, enabled)?;
265        }
266        Ok(())
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    struct RollbackEnvVar(String, Option<String>);
275
276    impl RollbackEnvVar {
277        pub fn new(name: &str) -> Self {
278            let old = std::env::var(name).ok();
279            RollbackEnvVar(name.to_string(), old)
280        }
281    }
282
283    impl Drop for RollbackEnvVar {
284        fn drop(&mut self) {
285            if let Some(old) = self.1.take() {
286                std::env::set_var(&self.0, old);
287            }
288        }
289    }
290
291    #[test]
292    fn ok_parse_experimental_features() {
293        let _old = RollbackEnvVar::new("FORC_EXPERIMENTAL");
294        let _old = RollbackEnvVar::new("FORC_NO_EXPERIMENTAL");
295
296        let mut features = ExperimentalFeatures {
297            new_encoding: false,
298            ..Default::default()
299        };
300
301        std::env::set_var("FORC_EXPERIMENTAL", "new_encoding");
302        std::env::set_var("FORC_NO_EXPERIMENTAL", "");
303        assert!(!features.new_encoding);
304        let _ = features.parse_from_environment_variables();
305        assert!(features.new_encoding);
306
307        std::env::set_var("FORC_EXPERIMENTAL", "");
308        std::env::set_var("FORC_NO_EXPERIMENTAL", "new_encoding");
309        assert!(features.new_encoding);
310        let _ = features.parse_from_environment_variables();
311        assert!(!features.new_encoding);
312    }
313}