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