Repository: bytecodealliance/wasm-tools
Pull request #2186 (2025-05-11 19:42:13)

Add the "lime1" feature set for validation
This commit adds `WasmFeatures::LIME1` which is a set of features added recently to LLVM to encompass a just-a-bit-more-than-MVP baseline which includes useful features for producers without placing too much of an implementation burden on engines. This required splitting the reference-types and bulk-memory features into sub-portions to handle the special cases of the lime1 proposal.

A new test is added here which asserts that all the right features are turned on while all the wrong features are indeed all turned off. This commit additionally includes a bug fix where passive data segments were not properly gated by the bulk-memory feature prior to this commit.


Patch:
diff --git a/crates/wasmparser/src/features.rs b/crates/wasmparser/src/features.rs
index 9e5ae374..2ef63ac5 100644
--- a/crates/wasmparser/src/features.rs
+++ b/crates/wasmparser/src/features.rs
@@ -147,11 +147,28 @@ define_wasm_features! {
         /// The WebAssembly `sign-extension-ops` proposal.
         pub sign_extension: SIGN_EXTENSION(1 << 2) = true;
         /// The WebAssembly reference types proposal.
-        pub reference_types: REFERENCE_TYPES(1 << 3) = true;
+        //
+        // Note that this is two bits, one for the proposal itself and one which
+        // is the overlong call-indirect encoding. This is split across two bits
+        // since the "lime1" feature set below only encompasses some of this
+        // feature, not all of it. Enabling reference types always enabled
+        // call-indirect-overlong, but not vice-versa.
+        //
+        // Also note that this goes against the recommendation of the
+        // `bitflags!` macro since the lone `1 << 3` bit here doesn't actually
+        // correspond to any named flag. That means it's possible to technically
+        // create a `WasmFeatures` that only has the `1 << 3` bit set, which
+        // doesn't actually mean anything, but that in theory shouldn't be too
+        // harmful and is esoteric enough we don't have to worry much about it.
+        pub reference_types: REFERENCE_TYPES((1 << 3) | Self::CALL_INDIRECT_OVERLONG.bits()) = true;
         /// The WebAssembly multi-value proposal.
         pub multi_value: MULTI_VALUE(1 << 4) = true;
         /// The WebAssembly bulk memory operations proposal.
-        pub bulk_memory: BULK_MEMORY(1 << 5) = true;
+        //
+        // Note that this proposal is split in two the same way
+        // `REFERENCE_TYPES` is split above. See more words there for why and
+        // rationale.
+        pub bulk_memory: BULK_MEMORY((1 << 5) | Self::BULK_MEMORY_OPT.bits()) = true;
         /// The WebAssembly SIMD proposal.
         pub simd: SIMD(1 << 6) = true;
         /// The WebAssembly Relaxed SIMD proposal.
@@ -264,6 +281,20 @@ define_wasm_features! {
         /// Corresponds to the 🛸 character in
         /// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md>.
         pub cm_gc: CM_GC(1 << 32) = false;
+
+        /// Subset of the reference-types WebAssembly proposal which only
+        /// encompasses the leb-encoding of the table immediate to the
+        /// `call_indirect` instruction, enabling over-long encodings of an
+        /// integer for example.
+        ///
+        /// This is a subcomponent of the "lime1" feature.
+        pub call_indirect_overlong: CALL_INDIRECT_OVERLONG(1 << 33) = true;
+
+        /// Subset of the bulk-memory proposal covering just the `memory.copy`
+        /// and `memory.fill` instructions.
+        ///
+        /// This is a subcomponent of the "lime1" feature.
+        pub bulk_memory_opt: BULK_MEMORY_OPT(1 << 34) = true;
     }
 }
 
@@ -320,6 +351,23 @@ impl WasmFeatures {
         .union(WasmFeatures::THREADS)
         .union(WasmFeatures::EXCEPTIONS)
         .union(WasmFeatures::MEMORY64);
+
+    /// The feature set associated with the "lime1" set of features.
+    ///
+    /// Here "lime1" stands for "linear memory version 1" and is a stable set of
+    /// features agreed on by both producers and consumers which is more than
+    /// the MVP but does not include some more weighty engine features such as
+    /// reference types, gc, etc.
+    ///
+    /// <https://github.com/WebAssembly/tool-conventions/blob/main/Lime.md>
+    #[cfg(feature = "features")]
+    pub const LIME1: WasmFeatures = WasmFeatures::WASM1
+        .union(WasmFeatures::MULTI_VALUE)
+        .union(WasmFeatures::SIGN_EXTENSION)
+        .union(WasmFeatures::SATURATING_FLOAT_TO_INT)
+        .union(WasmFeatures::BULK_MEMORY_OPT)
+        .union(WasmFeatures::EXTENDED_CONST)
+        .union(WasmFeatures::CALL_INDIRECT_OVERLONG);
 }
 
 #[cfg(feature = "features")]
diff --git a/crates/wasmparser/src/readers/core/operators.rs b/crates/wasmparser/src/readers/core/operators.rs
index d8e013ea..6488e299 100644
--- a/crates/wasmparser/src/readers/core/operators.rs
+++ b/crates/wasmparser/src/readers/core/operators.rs
@@ -547,7 +547,7 @@ impl<'a> OperatorsReader<'a> {
             0x10 => visitor.visit_call(self.reader.read_var_u32()?),
             0x11 => {
                 let index = self.reader.read_var_u32()?;
-                let table = self.read_table_index_or_zero_if_not_reference_types()?;
+                let table = self.read_call_indirect_table_immediate()?;
                 visitor.visit_call_indirect(index, table)
             }
             0x12 => visitor.visit_return_call(self.reader.read_var_u32()?),
@@ -1677,16 +1677,19 @@ impl<'a> OperatorsReader<'a> {
         }
     }
 
-    fn read_table_index_or_zero_if_not_reference_types(&mut self) -> Result<u32> {
-        if self.reader.reference_types() {
-            self.reader.read_var_u32()
-        } else {
-            // Before reference types this byte was required to be a single zero
-            // byte, not a LEB-encoded zero, so require a precise zero byte.
-            match self.reader.read_u8()? {
-                0 => Ok(0),
-                _ => bail!(self.original_position() - 1, "zero byte expected"),
-            }
+    fn read_call_indirect_table_immediate(&mut self) -> Result<u32> {
+        // If the `call_indirect_overlong` feature is enabled, then read this
+        // immediate as a LEB. This feature is enabled as part of the
+        // `reference_types` feature or the `lime1` feature.
+        if self.reader.call_indirect_overlong() {
+            return self.reader.read_var_u32();
+        }
+
+        // Before reference types this byte was required to be a single zero
+        // byte, not a LEB-encoded zero, so require a precise zero byte.
+        match self.reader.read_u8()? {
+            0 => Ok(0),
+            _ => bail!(self.original_position() - 1, "zero byte expected"),
         }
     }
 }
diff --git a/crates/wasmparser/src/validator/core.rs b/crates/wasmparser/src/validator/core.rs
index 849aac02..a8987698 100644
--- a/crates/wasmparser/src/validator/core.rs
+++ b/crates/wasmparser/src/validator/core.rs
@@ -102,7 +102,15 @@ impl ModuleState {
 
     pub fn add_data_segment(&mut self, data: Data, types: &TypeList, offset: usize) -> Result<()> {
         match data.kind {
-            DataKind::Passive => Ok(()),
+            DataKind::Passive => {
+                if !self.module.features.bulk_memory() {
+                    bail!(
+                        offset,
+                        "passive data segments require the bulk-memory proposal"
+                    );
+                }
+                Ok(())
+            }
             DataKind::Active {
                 memory_index,
                 offset_expr,
diff --git a/crates/wasmparser/src/validator/operators.rs b/crates/wasmparser/src/validator/operators.rs
index 02da1123..e36dfc1f 100644
--- a/crates/wasmparser/src/validator/operators.rs
+++ b/crates/wasmparser/src/validator/operators.rs
@@ -1659,6 +1659,13 @@ where
         self.push_operand(ValType::I64)?;
         Ok(())
     }
+
+    fn check_enabled(&self, flag: bool, desc: &str) -> Result<()> {
+        if flag {
+            return Ok(());
+        }
+        bail!(self.offset, "{desc} support is not enabled");
+    }
 }
 
 pub fn ty_to_str(ty: ValType) -> &'static str {
@@ -1684,29 +1691,27 @@ struct WasmProposalValidator<'validator, 'resources, T>(
     OperatorValidatorTemp<'validator, 'resources, T>,
 );
 
-impl<T> WasmProposalValidator<'_, '_, T> {
-    fn check_enabled(&self, flag: bool, desc: &str) -> Result<()> {
-        if flag {
-            return Ok(());
-        }
-        bail!(self.0.offset, "{desc} support is not enabled");
-    }
-}
-
 #[cfg_attr(not(feature = "simd"), allow(unused_macro_rules))]
 macro_rules! validate_proposal {
     ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {
         $(
             fn $visit(&mut self $($(,$arg: $argty)*)?) -> Result<()> {
-                validate_proposal!(validate self $proposal);
+                validate_proposal!(validate self $proposal / $op);
                 self.0.$visit($( $($arg),* )?)
             }
         )*
     };
 
-    (validate self mvp) => {};
-    (validate $self:ident $proposal:ident) => {
-        $self.check_enabled($self.0.features.$proposal(), validate_proposal!(desc $proposal))?
+    (validate self mvp / $op:ident) => {};
+
+    // These opcodes are handled specially below as they were introduced in the
+    // bulk memory proposal but are gated by the `bulk_memory_opt`
+    // "sub-proposal".
+    (validate self $proposal:ident / MemoryFill) => {};
+    (validate self $proposal:ident / MemoryCopy) => {};
+
+    (validate $self:ident $proposal:ident / $op:ident) => {
+        $self.0.check_enabled($self.0.features.$proposal(), validate_proposal!(desc $proposal))?
     };
 
     (desc simd) => ("SIMD");
@@ -3072,6 +3077,7 @@ where
         Ok(())
     }
     fn visit_memory_copy(&mut self, dst: u32, src: u32) -> Self::Output {
+        self.check_enabled(self.features.bulk_memory_opt(), "bulk memory")?;
         let dst_ty = self.check_memory_index(dst)?;
         let src_ty = self.check_memory_index(src)?;
 
@@ -3089,6 +3095,7 @@ where
         Ok(())
     }
     fn visit_memory_fill(&mut self, mem: u32) -> Self::Output {
+        self.check_enabled(self.features.bulk_memory_opt(), "bulk memory")?;
         let ty = self.check_memory_index(mem)?;
         self.pop_operand(Some(ty))?;
         self.pop_operand(Some(ValType::I32))?;
diff --git a/src/bin/wasm-tools/validate.rs b/src/bin/wasm-tools/validate.rs
index 712d0308..68274f09 100644
--- a/src/bin/wasm-tools/validate.rs
+++ b/src/bin/wasm-tools/validate.rs
@@ -236,6 +236,7 @@ fn parse_features(arg: &str) -> Result<Vec<FeatureAction>> {
         ("wasm1", WasmFeatures::WASM1),
         ("wasm2", WasmFeatures::WASM2),
         ("wasm3", WasmFeatures::WASM3),
+        ("lime1", WasmFeatures::LIME1),
     ];
 
     enum Action {


Test patch:
diff --git a/tests/cli/lime1.wast b/tests/cli/lime1.wast
new file mode 100644
index 00000000..ed8e76f0
--- /dev/null
+++ b/tests/cli/lime1.wast
@@ -0,0 +1,140 @@
+;; RUN: wast % -f lime1
+
+;; import/export of mutable globals is allowed
+(module
+  (import "" "" (global (mut i32)))
+  (global (export "") (mut i32) (i32.const 0))
+)
+
+;; saturating float-to-int instructions are allowed
+(module
+  (func (param f32) (result i32)
+    local.get 0
+    i32.trunc_sat_f32_s)
+)
+
+;; sign-extension is allowed
+(module
+  (func (param i32) (result i32)
+    local.get 0
+    i32.extend8_s)
+)
+
+;; multi-value is allowed
+(module (type (func (result i32 i32))))
+
+;; memory.copy and memory.fill are allowed ...
+(module
+  (memory 1)
+  (func (param i32 i32 i32)
+    local.get 0
+    local.get 1
+    local.get 2
+    memory.copy
+    local.get 0
+    local.get 1
+    local.get 2
+    memory.fill))
+
+;; ... but other bulk-memory features are not allowed
+(assert_invalid
+  (module (data))
+  "passive data segments require the bulk-memory proposal")
+(assert_invalid
+  (module (elem func))
+  "bulk memory must be enabled")
+(assert_invalid
+  (module (func data.drop 0))
+  "bulk memory support is not enabled")
+(assert_invalid
+  (module (func memory.init 0))
+  "bulk memory support is not enabled")
+(assert_invalid
+  (module (func table.init 0))
+  "bulk memory support is not enabled")
+(assert_invalid
+  (module (func elem.drop 0))
+  "bulk memory support is not enabled")
+(assert_invalid
+  (module (func table.copy))
+  "bulk memory support is not enabled")
+
+;; SIMD is not included
+(assert_invalid
+  (module (type (func (param v128))))
+  "SIMD support is not enabled")
+
+;; threads is not included
+(assert_invalid
+  (module (memory 1 1 shared))
+  "threads must be enabled for shared memories")
+
+;; tail call not enabled
+(assert_invalid
+  (module (func return_call 0))
+  "tail calls support is not enabled")
+
+;; floats are allowed
+(module (func (param f32)))
+
+;; multi memory not enabled
+(assert_invalid
+  (module (memory 1) (memory 1))
+  "multiple memories")
+
+;; exceptions not enabled
+(assert_invalid
+  (module (tag))
+  "exceptions proposal not enabled")
+
+;; memory64 not enabled
+(assert_invalid
+  (module (memory i64 1))
+  "memory64 must be enabled for 64-bit memories")
+
+;; extended const is allowed
+(module
+  (global i32 (i32.add (i32.const 0) (i32.const 1))))
+
+;; GC is not allowed
+(assert_invalid
+  (module (type (sub (func))))
+  "gc proposal must be enabled to use subtypes")
+
+;; reference types is mostly not allowed...
+(assert_invalid
+  (module (type (func (param externref))))
+  "reference types support is not enabled")
+(assert_invalid
+  (module (type (func (param funcref))))
+  "reference types support is not enabled")
+(assert_invalid
+  (module (table 1 funcref) (table 1 funcref))
+  "multiple tables")
+
+;; ... but overlong encodings of `call_indirect` table immediates are allowed.
+(module binary
+  "\00asm" "\01\00\00\00" ;; magic header
+  "\01\04"    ;; type section, 4 bytes
+  "\01"       ;; 1 type
+  "\60\00\00" ;; function type, no params, no results
+  "\03\02"    ;; function section, 2 bytes
+  "\01"       ;; 1 function
+  "\00"       ;; function0 has type 0
+  "\04\04"    ;; table section, 4 bytes
+  "\01"       ;; 1 table
+  "\70\00\01" ;; funcref table, no flags, min 1 element
+  "\0a\0c"    ;; code section, 12 bytes
+  "\01"       ;; 1 function
+  "\0a"       ;; function is 10 bytes
+  "\00"       ;; no locals
+  "\41\00"    ;; i32.const 0
+
+  ;; call_indirect opcode + (type 0) immediate
+  "\11\00"
+
+  ;; 4-byte encoding of the immediate 0, the table index for `call_indirect`
+  "\80\80\80\00"
+
+  "\0b"       ;; end
+)
diff --git a/tests/cli/missing-features/bulk-memory.wast b/tests/cli/missing-features/bulk-memory.wast
new file mode 100644
index 00000000..d12533d8
--- /dev/null
+++ b/tests/cli/missing-features/bulk-memory.wast
@@ -0,0 +1,21 @@
+;; RUN: wast % -f=-bulk-memory
+
+(assert_invalid
+  (module
+    (memory 1)
+    (func (param i32 i32 i32)
+      local.get 0
+      local.get 1
+      local.get 2
+      memory.copy))
+  "bulk memory support is not enabled")
+
+(assert_invalid
+  (module
+    (memory 1)
+    (func (param i32 i32 i32)
+      local.get 0
+      local.get 1
+      local.get 2
+      memory.fill))
+  "bulk memory support is not enabled")
diff --git a/tests/cli/validate-unknown-features.wat.stderr b/tests/cli/validate-unknown-features.wat.stderr
index b14cec9e..8b93bad8 100644
--- a/tests/cli/validate-unknown-features.wat.stderr
+++ b/tests/cli/validate-unknown-features.wat.stderr
@@ -1,4 +1,4 @@
 error: invalid value 'unknown' for '--features <FEATURES>': unknown feature `unknown`
-Valid features: mutable-global, saturating-float-to-int, sign-extension, reference-types, multi-value, bulk-memory, simd, relaxed-simd, threads, shared-everything-threads, tail-call, floats, multi-memory, exceptions, memory64, extended-const, component-model, function-references, memory-control, gc, custom-page-sizes, legacy-exceptions, gc-types, stack-switching, wide-arithmetic, cm-values, cm-nested-names, cm-async, cm-async-stackful, cm-async-builtins, cm-error-context, cm-fixed-size-list, cm-gc, mvp, wasm1, wasm2, wasm3, all
+Valid features: mutable-global, saturating-float-to-int, sign-extension, reference-types, multi-value, bulk-memory, simd, relaxed-simd, threads, shared-everything-threads, tail-call, floats, multi-memory, exceptions, memory64, extended-const, component-model, function-references, memory-control, gc, custom-page-sizes, legacy-exceptions, gc-types, stack-switching, wide-arithmetic, cm-values, cm-nested-names, cm-async, cm-async-stackful, cm-async-builtins, cm-error-context, cm-fixed-size-list, cm-gc, call-indirect-overlong, bulk-memory-opt, mvp, wasm1, wasm2, wasm3, lime1, all
 
 For more information, try '--help'.