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 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 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 #[clap(long, value_delimiter = ',')]
182 pub experimental: Vec<Feature>,
183
184 #[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 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}