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