Skip to main content

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    new_hashing = true,
173    "https://github.com/FuelLabs/sway/issues/7256",
174    str_array_no_padding = false,
175    "https://github.com/FuelLabs/sway/issues/7528",
176    dynamic_storage = false,
177    "https://github.com/FuelLabs/sway/issues/7560",
178}
179
180#[derive(Clone, Debug, Default, Parser)]
181pub struct CliFields {
182    /// Comma separated list of all experimental features that will be enabled
183    #[clap(long, value_delimiter = ',')]
184    pub experimental: Vec<Feature>,
185
186    /// Comma separated list of all experimental features that will be disabled
187    #[clap(long, value_delimiter = ',')]
188    pub no_experimental: Vec<Feature>,
189}
190
191impl CliFields {
192    pub fn experimental_as_cli_string(&self) -> Option<String> {
193        Self::features_as_cli_string(&self.experimental)
194    }
195
196    pub fn no_experimental_as_cli_string(&self) -> Option<String> {
197        Self::features_as_cli_string(&self.no_experimental)
198    }
199
200    fn features_as_cli_string(features: &[Feature]) -> Option<String> {
201        if features.is_empty() {
202            None
203        } else {
204            Some(
205                features
206                    .iter()
207                    .map(|f| f.name())
208                    .collect::<Vec<_>>()
209                    .join(","),
210            )
211        }
212    }
213}
214
215#[derive(Debug)]
216pub enum Error {
217    ParseError(String),
218    UnknownFeature(String),
219}
220
221impl std::fmt::Display for Error {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self {
224            Error::ParseError(feature) => f.write_fmt(format_args!(
225                "Experimental feature \"{feature}\" cannot be parsed."
226            )),
227            Error::UnknownFeature(feature) => {
228                f.write_fmt(format_args!("Unknown experimental feature: \"{feature}\"."))
229            }
230        }
231    }
232}
233
234impl ExperimentalFeatures {
235    #[allow(clippy::iter_over_hash_type)]
236    pub fn parse_from_package_manifest(
237        &mut self,
238        experimental: &std::collections::HashMap<String, bool>,
239    ) -> Result<(), Error> {
240        for (feature, enabled) in experimental {
241            self.set_enabled_by_name(feature, *enabled)?;
242        }
243        Ok(())
244    }
245
246    /// Enable and disable features using comma separated feature names from
247    /// environment variables "FORC_EXPERIMENTAL" and "FORC_NO_EXPERIMENTAL".
248    pub fn parse_from_environment_variables(&mut self) -> Result<(), Error> {
249        if let Ok(features) = std::env::var("FORC_NO_EXPERIMENTAL") {
250            self.parse_comma_separated_list(&features, false)?;
251        }
252
253        if let Ok(features) = std::env::var("FORC_EXPERIMENTAL") {
254            self.parse_comma_separated_list(&features, true)?;
255        }
256
257        Ok(())
258    }
259
260    pub fn parse_comma_separated_list(
261        &mut self,
262        features: impl AsRef<str>,
263        enabled: bool,
264    ) -> Result<(), Error> {
265        for feature in features.as_ref().split(',') {
266            self.set_enabled_by_name(feature, enabled)?;
267        }
268        Ok(())
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    struct RollbackEnvVar(String, Option<String>);
277
278    impl RollbackEnvVar {
279        pub fn new(name: &str) -> Self {
280            let old = std::env::var(name).ok();
281            RollbackEnvVar(name.to_string(), old)
282        }
283    }
284
285    impl Drop for RollbackEnvVar {
286        fn drop(&mut self) {
287            if let Some(old) = self.1.take() {
288                std::env::set_var(&self.0, old);
289            }
290        }
291    }
292
293    #[test]
294    fn ok_parse_experimental_features() {
295        let _old = RollbackEnvVar::new("FORC_EXPERIMENTAL");
296        let _old = RollbackEnvVar::new("FORC_NO_EXPERIMENTAL");
297
298        let mut features = ExperimentalFeatures {
299            new_encoding: false,
300            ..Default::default()
301        };
302
303        std::env::set_var("FORC_EXPERIMENTAL", "new_encoding");
304        std::env::set_var("FORC_NO_EXPERIMENTAL", "");
305        assert!(!features.new_encoding);
306        let _ = features.parse_from_environment_variables();
307        assert!(features.new_encoding);
308
309        std::env::set_var("FORC_EXPERIMENTAL", "");
310        std::env::set_var("FORC_NO_EXPERIMENTAL", "new_encoding");
311        assert!(features.new_encoding);
312        let _ = features.parse_from_environment_variables();
313        assert!(!features.new_encoding);
314    }
315}