Repository: bytecodealliance/wasm-tools
Pull request #1862 (2024-10-11 02:22:35)

Add support for `extended-const` to `wasm-smith`
This commit adds support for the `extended-const` proposal to the `wasm-smith` generator crate. This is gated behind a new `extended_const_enabled` field in `wasm_smith::Config`

This commit additionally refactors the `wasm-smith` test suite and fuzzers in this repository to share the same source for generating a `WasmFeatures` from a `Config`.

Finally this also fixes a few minor issues with wide arithmetic, namely it was enabled in fuzzing for components by accident and needed to be added to the `Config`-to-`WasmFeatures` conversion.


Patch:
diff --git a/README.md b/README.md
index bfa051bf..cbf1a5bb 100644
--- a/README.md
+++ b/README.md
@@ -187,6 +187,8 @@ Currently implemented proposals in this repository that are stage 4+ are:
   this is an exception, it's not stage4+ but is on-by-default)
 * [x] [exceptions](https://github.com/WebAssembly/exceptions)
 * [x] [extended-const](https://github.com/WebAssembly/extended-const)
+* [x] [extended-name-section](https://github.com/WebAssembly/extended-name-section)
+  (note this is not phase 4 but `wast` does not have the concept of features)
 * [x] [function-references](https://github.com/WebAssembly/function-references)
 * [x] [gc](https://github.com/WebAssembly/gc)
 * [x] [multi-memory](https://github.com/WebAssembly/multi-memory)
@@ -199,6 +201,8 @@ Currently implemented proposals in this repository that are stage 4+ are:
 * [x] [simd](https://github.com/WebAssembly/simd)
 * [x] [tail-call](https://github.com/WebAssembly/tail-call)
 * [x] [threads](https://github.com/WebAssembly/threads)
+* [x] [wat-numeric-values](https://github.com/WebAssembly/wat-numeric-values)
+  (note this is not phase 4 but `wast` does not have the concept of features)
 
 These proposals are implemented but have not yet reached stage 4. This means
 they are disabled by default in validation and their implementation may change
diff --git a/crates/wasm-encoder/src/core/code.rs b/crates/wasm-encoder/src/core/code.rs
index 4ff071b1..c2bcdafd 100644
--- a/crates/wasm-encoder/src/core/code.rs
+++ b/crates/wasm-encoder/src/core/code.rs
@@ -3861,6 +3861,15 @@ impl ConstExpr {
         }
     }
 
+    /// Create a constant expression with the sequence of instructions
+    pub fn extended<'a>(insns: impl IntoIterator<Item = Instruction<'a>>) -> Self {
+        let mut bytes = vec![];
+        for insn in insns {
+            insn.encode(&mut bytes);
+        }
+        Self { bytes }
+    }
+
     fn new_insn(insn: Instruction) -> Self {
         let mut bytes = vec![];
         insn.encode(&mut bytes);
diff --git a/crates/wasm-smith/Cargo.toml b/crates/wasm-smith/Cargo.toml
index 6bb40b98..f7275ff3 100644
--- a/crates/wasm-smith/Cargo.toml
+++ b/crates/wasm-smith/Cargo.toml
@@ -32,7 +32,7 @@ leb128 = { workspace = true }
 serde = { workspace = true, optional = true }
 serde_derive = { workspace = true, optional = true }
 wasm-encoder = { workspace = true }
-wasmparser = { workspace = true, optional = true, features = ['validate'] }
+wasmparser = { workspace = true, optional = true, features = ['validate', 'features'] }
 wat = { workspace = true, optional = true }
 
 [dev-dependencies]
diff --git a/crates/wasm-smith/src/config.rs b/crates/wasm-smith/src/config.rs
index 1d4a6d8e..7702a170 100644
--- a/crates/wasm-smith/src/config.rs
+++ b/crates/wasm-smith/src/config.rs
@@ -606,6 +606,13 @@ define_config! {
         ///
         /// Defaults to `false`.
         pub wide_arithmetic_enabled: bool = false,
+
+        /// Determines whether the [extended-const proposal] is enabled.
+        ///
+        /// [extended-const proposal]: https://github.com/WebAssembly/extended-const
+        ///
+        /// Defaults to `true`.
+        pub extended_const_enabled: bool = true,
     }
 }
 
@@ -712,6 +719,7 @@ impl<'a> Arbitrary<'a> for Config {
             max_table_elements: u.int_in_range(0..=1_000_000)?,
             disallow_traps: u.arbitrary()?,
             allow_floats: u.arbitrary()?,
+            extended_const_enabled: u.arbitrary()?,
 
             // These fields, unlike the ones above, are less useful to set.
             // They either make weird inputs or are for features not widely
@@ -779,4 +787,47 @@ impl Config {
             self.relaxed_simd_enabled = false;
         }
     }
+
+    /// Returns the set of features that are necessary for validating against
+    /// this `Config`.
+    #[cfg(feature = "wasmparser")]
+    pub fn features(&self) -> wasmparser::WasmFeatures {
+        use wasmparser::WasmFeatures;
+
+        // Currently wasm-smith doesn't have knobs for the MVP (floats) or
+        // `mutable-global`. These are unconditionally enabled.
+        let mut features = WasmFeatures::MUTABLE_GLOBAL | WasmFeatures::WASM1;
+
+        // All other features that can be generated by wasm-smith are gated by
+        // configuration fields. Conditionally set each feature based on the
+        // status of the fields in `self`.
+        features.set(
+            WasmFeatures::SATURATING_FLOAT_TO_INT,
+            self.saturating_float_to_int_enabled,
+        );
+        features.set(
+            WasmFeatures::SIGN_EXTENSION,
+            self.sign_extension_ops_enabled,
+        );
+        features.set(WasmFeatures::REFERENCE_TYPES, self.reference_types_enabled);
+        features.set(WasmFeatures::MULTI_VALUE, self.multi_value_enabled);
+        features.set(WasmFeatures::BULK_MEMORY, self.bulk_memory_enabled);
+        features.set(WasmFeatures::SIMD, self.simd_enabled);
+        features.set(WasmFeatures::RELAXED_SIMD, self.relaxed_simd_enabled);
+        features.set(WasmFeatures::MULTI_MEMORY, self.max_memories > 1);
+        features.set(WasmFeatures::EXCEPTIONS, self.exceptions_enabled);
+        features.set(WasmFeatures::MEMORY64, self.memory64_enabled);
+        features.set(WasmFeatures::TAIL_CALL, self.tail_call_enabled);
+        features.set(WasmFeatures::FUNCTION_REFERENCES, self.gc_enabled);
+        features.set(WasmFeatures::GC, self.gc_enabled);
+        features.set(WasmFeatures::THREADS, self.threads_enabled);
+        features.set(
+            WasmFeatures::CUSTOM_PAGE_SIZES,
+            self.custom_page_sizes_enabled,
+        );
+        features.set(WasmFeatures::EXTENDED_CONST, self.extended_const_enabled);
+        features.set(WasmFeatures::WIDE_ARITHMETIC, self.wide_arithmetic_enabled);
+
+        features
+    }
 }
diff --git a/crates/wasm-smith/src/core.rs b/crates/wasm-smith/src/core.rs
index 37f41462..b5430b09 100644
--- a/crates/wasm-smith/src/core.rs
+++ b/crates/wasm-smith/src/core.rs
@@ -1664,8 +1664,18 @@ impl Module {
         // type of that value.
         let ty = self.arbitrary_matching_val_type(u, ty)?;
         match ty {
-            ValType::I32 => choices.push(Box::new(|u, _| Ok(ConstExpr::i32_const(u.arbitrary()?)))),
-            ValType::I64 => choices.push(Box::new(|u, _| Ok(ConstExpr::i64_const(u.arbitrary()?)))),
+            ValType::I32 => {
+                choices.push(Box::new(|u, _| Ok(ConstExpr::i32_const(u.arbitrary()?))));
+                if self.config.extended_const_enabled {
+                    choices.push(Box::new(arbitrary_extended_const));
+                }
+            }
+            ValType::I64 => {
+                choices.push(Box::new(|u, _| Ok(ConstExpr::i64_const(u.arbitrary()?))));
+                if self.config.extended_const_enabled {
+                    choices.push(Box::new(arbitrary_extended_const));
+                }
+            }
             ValType::F32 => choices.push(Box::new(|u, _| Ok(ConstExpr::f32_const(u.arbitrary()?)))),
             ValType::F64 => choices.push(Box::new(|u, _| Ok(ConstExpr::f64_const(u.arbitrary()?)))),
             ValType::V128 => {
@@ -1707,7 +1717,68 @@ impl Module {
         let f = u.choose(&choices)?;
         let ret = f(u, ty);
         self.const_expr_choices = choices;
-        ret
+        return ret;
+
+        /// Implementation of generation of expressions from the
+        /// `extended-const` proposal to WebAssembly. This proposal enabled
+        /// using `i{32,64}.{add,sub,mul}` in constant expressions in addition
+        /// to the previous `i{32,64}.const` instructions. Note that at this
+        /// time this doesn't use the full expression generator in
+        /// `code_builder.rs` but instead inlines just what's necessary for
+        /// constant expressions here.
+        fn arbitrary_extended_const(u: &mut Unstructured<'_>, ty: ValType) -> Result<ConstExpr> {
+            use wasm_encoder::Instruction::*;
+
+            // This only works for i32/i64, would need refactoring for different
+            // types.
+            assert!(ty == ValType::I32 || ty == ValType::I64);
+            let add = if ty == ValType::I32 { I32Add } else { I64Add };
+            let sub = if ty == ValType::I32 { I32Sub } else { I64Sub };
+            let mul = if ty == ValType::I32 { I32Mul } else { I64Mul };
+            let const_: fn(&mut Unstructured<'_>) -> Result<wasm_encoder::Instruction<'static>> =
+                if ty == ValType::I32 {
+                    |u| u.arbitrary().map(I32Const)
+                } else {
+                    |u| u.arbitrary().map(I64Const)
+                };
+
+            // Here `instrs` is the list of instructions, in reverse order, that
+            // are going to be emitted. The `needed` value keeps track of how
+            // many values are needed to complete this expression. New
+            // instructions must be generated while some more items are needed.
+            let mut instrs = Vec::new();
+            let mut needed = 1;
+            while needed > 0 {
+                // If fuzz data has been exhausted or if this is a "large
+                // enough" constant expression then force generation of
+                // constants to finish out the expression.
+                let choice = if u.is_empty() || instrs.len() > 10 {
+                    0
+                } else {
+                    u.int_in_range(0..=3)?
+                };
+                match choice {
+                    0 => {
+                        instrs.push(const_(u)?);
+                        needed -= 1;
+                    }
+                    1 => {
+                        instrs.push(add.clone());
+                        needed += 1;
+                    }
+                    2 => {
+                        instrs.push(sub.clone());
+                        needed += 1;
+                    }
+                    3 => {
+                        instrs.push(mul.clone());
+                        needed += 1;
+                    }
+                    _ => unreachable!(),
+                }
+            }
+            Ok(ConstExpr::extended(instrs.into_iter().rev()))
+        }
     }
 
     fn arbitrary_globals(&mut self, u: &mut Unstructured) -> Result<()> {
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index eb5d0e08..6b39f556 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -18,7 +18,7 @@ libfuzzer-sys = { workspace = true }
 log = { workspace = true }
 tempfile = "3.0"
 wasm-mutate = { workspace = true }
-wasm-smith = { workspace = true, features = ['component-model'] }
+wasm-smith = { workspace = true, features = ['component-model', 'wasmparser'] }
 wasmparser = { workspace = true, features = ['features'] }
 wasmprinter = { workspace = true }
 wasmtime = { workspace = true, optional = true }
diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs
index 9130954a..f0d70ed9 100644
--- a/fuzz/src/lib.rs
+++ b/fuzz/src/lib.rs
@@ -2,7 +2,6 @@ use libfuzzer_sys::arbitrary::{Result, Unstructured};
 use std::fmt::Debug;
 use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
 use wasm_smith::{Component, Config, Module};
-use wasmparser::WasmFeatures;
 
 pub mod incremental_parse;
 pub mod mutate;
@@ -27,6 +26,7 @@ pub fn generate_valid_module(
     config.memory64_enabled = u.arbitrary()?;
     config.canonicalize_nans = u.arbitrary()?;
     config.custom_page_sizes_enabled = u.arbitrary()?;
+    config.wide_arithmetic_enabled = u.arbitrary()?;
 
     configure(&mut config, u)?;
 
@@ -60,7 +60,6 @@ pub fn generate_valid_component(
     config.memory64_enabled = u.arbitrary()?;
     config.exceptions_enabled = u.arbitrary()?;
     config.canonicalize_nans = u.arbitrary()?;
-    config.wide_arithmetic_enabled = u.arbitrary()?;
 
     configure(&mut config, u)?;
 
@@ -75,48 +74,7 @@ pub fn generate_valid_component(
 }
 
 pub fn validator_for_config(config: &Config) -> wasmparser::Validator {
-    // Start with the bare-bones set of features that wasm started with. Then
-    // wasm-smith doesn't have knobs to enable/disable mutable globals so
-    // unconditionally enable that as well.
-    let mut features = WasmFeatures::WASM1 | WasmFeatures::MUTABLE_GLOBAL;
-
-    // Next conditionally enable/disable features based on `config`.
-    features.set(
-        WasmFeatures::SIGN_EXTENSION,
-        config.sign_extension_ops_enabled,
-    );
-    features.set(WasmFeatures::TAIL_CALL, config.tail_call_enabled);
-    features.set(
-        WasmFeatures::SATURATING_FLOAT_TO_INT,
-        config.saturating_float_to_int_enabled,
-    );
-    features.set(WasmFeatures::MULTI_VALUE, config.multi_value_enabled);
-    features.set(WasmFeatures::MULTI_MEMORY, config.max_memories > 1);
-    features.set(WasmFeatures::BULK_MEMORY, config.bulk_memory_enabled);
-    features.set(
-        WasmFeatures::REFERENCE_TYPES,
-        config.reference_types_enabled,
-    );
-    features.set(WasmFeatures::SIMD, config.simd_enabled);
-    features.set(WasmFeatures::RELAXED_SIMD, config.relaxed_simd_enabled);
-    features.set(WasmFeatures::MEMORY64, config.memory64_enabled);
-    features.set(WasmFeatures::THREADS, config.threads_enabled);
-    features.set(WasmFeatures::EXCEPTIONS, config.exceptions_enabled);
-    features.set(
-        WasmFeatures::CUSTOM_PAGE_SIZES,
-        config.custom_page_sizes_enabled,
-    );
-    // TODO: determine our larger story for function-references in
-    // wasm-tools and whether we should just have a Wasm GC flag since
-    // function-references is effectively part of the Wasm GC proposal at
-    // this point.
-    features.set(WasmFeatures::FUNCTION_REFERENCES, config.gc_enabled);
-    features.set(WasmFeatures::GC, config.gc_enabled);
-    features.set(
-        WasmFeatures::WIDE_ARITHMETIC,
-        config.wide_arithmetic_enabled,
-    );
-    wasmparser::Validator::new_with_features(features)
+    wasmparser::Validator::new_with_features(config.features())
 }
 
 // Optionally log the module and its configuration if we've gotten this


Test patch:
diff --git a/crates/wasm-smith/tests/available_imports.rs b/crates/wasm-smith/tests/available_imports.rs
index 27e4852d..a53bfcb8 100644
--- a/crates/wasm-smith/tests/available_imports.rs
+++ b/crates/wasm-smith/tests/available_imports.rs
@@ -8,7 +8,7 @@ use wasmparser::Validator;
 use wasmparser::{Parser, TypeRef, ValType};
 
 mod common;
-use common::{parser_features_from_config, validate};
+use common::validate;
 
 #[test]
 fn smoke_test_imports_config() {
@@ -21,7 +21,7 @@ fn smoke_test_imports_config() {
 
         let mut u = Unstructured::new(&buf);
         let (config, available) = import_config(&mut u);
-        let features = parser_features_from_config(&config);
+        let features = config.features();
 
         if let Ok(module) = Module::new(config, &mut u) {
             let wasm_bytes = module.to_bytes();
diff --git a/crates/wasm-smith/tests/common/mod.rs b/crates/wasm-smith/tests/common/mod.rs
index ae8cf676..4b611859 100644
--- a/crates/wasm-smith/tests/common/mod.rs
+++ b/crates/wasm-smith/tests/common/mod.rs
@@ -1,37 +1,4 @@
-use wasm_smith::Config;
-use wasmparser::{types::Types, Validator, WasmFeatures};
-
-pub fn parser_features_from_config(config: &Config) -> WasmFeatures {
-    let mut features = WasmFeatures::MUTABLE_GLOBAL | WasmFeatures::WASM1;
-    features.set(
-        WasmFeatures::SATURATING_FLOAT_TO_INT,
-        config.saturating_float_to_int_enabled,
-    );
-    features.set(
-        WasmFeatures::SIGN_EXTENSION,
-        config.sign_extension_ops_enabled,
-    );
-    features.set(
-        WasmFeatures::REFERENCE_TYPES,
-        config.reference_types_enabled,
-    );
-    features.set(WasmFeatures::MULTI_VALUE, config.multi_value_enabled);
-    features.set(WasmFeatures::BULK_MEMORY, config.bulk_memory_enabled);
-    features.set(WasmFeatures::SIMD, config.simd_enabled);
-    features.set(WasmFeatures::RELAXED_SIMD, config.relaxed_simd_enabled);
-    features.set(WasmFeatures::MULTI_MEMORY, config.max_memories > 1);
-    features.set(WasmFeatures::EXCEPTIONS, config.exceptions_enabled);
-    features.set(WasmFeatures::MEMORY64, config.memory64_enabled);
-    features.set(WasmFeatures::TAIL_CALL, config.tail_call_enabled);
-    features.set(WasmFeatures::FUNCTION_REFERENCES, config.gc_enabled);
-    features.set(WasmFeatures::GC, config.gc_enabled);
-    features.set(WasmFeatures::THREADS, config.threads_enabled);
-    features.set(
-        WasmFeatures::CUSTOM_PAGE_SIZES,
-        config.custom_page_sizes_enabled,
-    );
-    features
-}
+use wasmparser::{types::Types, Validator};
 
 pub fn validate(validator: &mut Validator, bytes: &[u8]) -> Types {
     let err = match validator.validate_all(bytes) {
diff --git a/crates/wasm-smith/tests/core.rs b/crates/wasm-smith/tests/core.rs
index d78d7876..88ef39f1 100644
--- a/crates/wasm-smith/tests/core.rs
+++ b/crates/wasm-smith/tests/core.rs
@@ -4,7 +4,7 @@ use wasm_smith::{Config, Module};
 use wasmparser::{Validator, WasmFeatures};
 
 mod common;
-use common::{parser_features_from_config, validate};
+use common::validate;
 
 #[test]
 fn smoke_test_module() {
@@ -77,6 +77,7 @@ fn multi_value_disabled() {
 }
 
 #[test]
+#[cfg(feature = "wasmparser")]
 fn smoke_can_smith_valid_webassembly_one_point_oh() {
     let mut rng = SmallRng::seed_from_u64(42);
     let mut buf = vec![0; 10240];
@@ -97,7 +98,7 @@ fn smoke_can_smith_valid_webassembly_one_point_oh() {
         cfg.gc_enabled = false;
         cfg.max_memories = 1;
         cfg.max_tables = 1;
-        let features = parser_features_from_config(&cfg);
+        let features = cfg.features();
         if let Ok(module) = Module::new(cfg, &mut u) {
             let wasm_bytes = module.to_bytes();
             // This table should set to `true` only features specified in wasm-core-1 spec.
diff --git a/crates/wasm-smith/tests/exports.rs b/crates/wasm-smith/tests/exports.rs
index 46a1a785..f43b9bf7 100644
--- a/crates/wasm-smith/tests/exports.rs
+++ b/crates/wasm-smith/tests/exports.rs
@@ -8,7 +8,7 @@ use wasmparser::{
 };
 
 mod common;
-use common::{parser_features_from_config, validate};
+use common::validate;
 
 #[derive(Debug, PartialEq)]
 enum ExportType {
@@ -144,7 +144,7 @@ fn smoke_test_exports(exports_test_case: &str, seed: u64) {
         let mut config = Config::arbitrary(&mut u).expect("arbitrary config");
         config.exports = Some(wasm.clone());
 
-        let features = parser_features_from_config(&config);
+        let features = config.features();
         let module = Module::new(config, &mut u).unwrap();
         let wasm_bytes = module.to_bytes();