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 = false,
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
189#[derive(Debug)]
190pub enum Error {
191    ParseError(String),
192    UnknownFeature(String),
193}
194
195impl std::fmt::Display for Error {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        match self {
198            Error::ParseError(feature) => f.write_fmt(format_args!(
199                "Experimental feature \"{feature}\" cannot be parsed."
200            )),
201            Error::UnknownFeature(feature) => {
202                f.write_fmt(format_args!("Unknown experimental feature: \"{feature}\"."))
203            }
204        }
205    }
206}
207
208impl ExperimentalFeatures {
209    pub fn parse_from_package_manifest(
210        &mut self,
211        experimental: &std::collections::HashMap<String, bool>,
212    ) -> Result<(), Error> {
213        for (feature, enabled) in experimental {
214            self.set_enabled_by_name(feature, *enabled)?;
215        }
216        Ok(())
217    }
218
219    /// Enable and disable features using comma separated feature names from
220    /// environment variables "FORC_EXPERIMENTAL" and "FORC_NO_EXPERIMENTAL".
221    pub fn parse_from_environment_variables(&mut self) -> Result<(), Error> {
222        if let Ok(features) = std::env::var("FORC_NO_EXPERIMENTAL") {
223            self.parse_comma_separated_list(&features, false)?;
224        }
225
226        if let Ok(features) = std::env::var("FORC_EXPERIMENTAL") {
227            self.parse_comma_separated_list(&features, true)?;
228        }
229
230        Ok(())
231    }
232
233    pub fn parse_comma_separated_list(
234        &mut self,
235        features: impl AsRef<str>,
236        enabled: bool,
237    ) -> Result<(), Error> {
238        for feature in features.as_ref().split(',') {
239            self.set_enabled_by_name(feature, enabled)?;
240        }
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    struct RollbackEnvVar(String, Option<String>);
250
251    impl RollbackEnvVar {
252        pub fn new(name: &str) -> Self {
253            let old = std::env::var(name).ok();
254            RollbackEnvVar(name.to_string(), old)
255        }
256    }
257
258    impl Drop for RollbackEnvVar {
259        fn drop(&mut self) {
260            if let Some(old) = self.1.take() {
261                std::env::set_var(&self.0, old);
262            }
263        }
264    }
265
266    #[test]
267    fn ok_parse_experimental_features() {
268        let _old = RollbackEnvVar::new("FORC_EXPERIMENTAL");
269        let _old = RollbackEnvVar::new("FORC_NO_EXPERIMENTAL");
270
271        let mut features = ExperimentalFeatures {
272            new_encoding: false,
273            ..Default::default()
274        };
275
276        std::env::set_var("FORC_EXPERIMENTAL", "new_encoding");
277        std::env::set_var("FORC_NO_EXPERIMENTAL", "");
278        assert!(!features.new_encoding);
279        let _ = features.parse_from_environment_variables();
280        assert!(features.new_encoding);
281
282        std::env::set_var("FORC_EXPERIMENTAL", "");
283        std::env::set_var("FORC_NO_EXPERIMENTAL", "new_encoding");
284        assert!(features.new_encoding);
285        let _ = features.parse_from_environment_variables();
286        assert!(!features.new_encoding);
287    }
288}