Repository: bytecodealliance/wasmtime
Pull request #8162 (2024-03-18 04:13:15)

Refactor the `MemFlags` internal representation


This commit refactors the internal `MemFlags` bits to be not just flags.
Instead some bits are now grouped together and interpreted as a unit.
This enables two primary API changes:

* First the `heap`/`table`/`vmctx` split is now represented as an "alias
  region" which is set as an enum. This means that all `MemFlags` carry
  an `Option<AliasRegion>` internally.

* Second trapping state is now represented as an `Option<TrapCode>`.
  This means that `notrap` is no longer a flag and `tabletrap` is no
  longer a flag. This does enable storing arbitrary trap codes though so
  long as they aren't `TrapCode::User(_)`.

The main purpose of this commit is to enable using more trap codes with
`MemFlags` for when a segfault is detected. For example with https://github.com/bytecodealliance/wasmtime/issues/5291 we
want a segfault to indicate a call-to-null, which is not currently
covered by `MemFlags`.


Patch:
diff --git a/cranelift/codegen/src/alias_analysis.rs b/cranelift/codegen/src/alias_analysis.rs
index f3b6339f9..f33f6eebf 100644
--- a/cranelift/codegen/src/alias_analysis.rs
+++ b/cranelift/codegen/src/alias_analysis.rs
@@ -68,7 +68,7 @@ use crate::{
     inst_predicates::{
         has_memory_fence_semantics, inst_addr_offset_type, inst_store_data, visit_block_succs,
     },
-    ir::{immediates::Offset32, Block, Function, Inst, Opcode, Type, Value},
+    ir::{immediates::Offset32, AliasRegion, Block, Function, Inst, Opcode, Type, Value},
     trace,
 };
 use cranelift_entity::{packed_option::PackedOption, EntityRef};
@@ -93,14 +93,11 @@ impl LastStores {
             self.other = inst.into();
         } else if opcode.can_store() {
             if let Some(memflags) = func.dfg.insts[inst].memflags() {
-                if memflags.heap() {
-                    self.heap = inst.into();
-                } else if memflags.table() {
-                    self.table = inst.into();
-                } else if memflags.vmctx() {
-                    self.vmctx = inst.into();
-                } else {
-                    self.other = inst.into();
+                match memflags.alias_region() {
+                    None => self.other = inst.into(),
+                    Some(AliasRegion::Heap) => self.heap = inst.into(),
+                    Some(AliasRegion::Table) => self.table = inst.into(),
+                    Some(AliasRegion::Vmctx) => self.vmctx = inst.into(),
                 }
             } else {
                 self.heap = inst.into();
@@ -113,14 +110,11 @@ impl LastStores {
 
     fn get_last_store(&self, func: &Function, inst: Inst) -> PackedOption<Inst> {
         if let Some(memflags) = func.dfg.insts[inst].memflags() {
-            if memflags.heap() {
-                self.heap
-            } else if memflags.table() {
-                self.table
-            } else if memflags.vmctx() {
-                self.vmctx
-            } else {
-                self.other
+            match memflags.alias_region() {
+                None => self.other,
+                Some(AliasRegion::Heap) => self.heap,
+                Some(AliasRegion::Table) => self.table,
+                Some(AliasRegion::Vmctx) => self.vmctx,
             }
         } else if func.dfg.insts[inst].opcode().can_load()
             || func.dfg.insts[inst].opcode().can_store()
diff --git a/cranelift/codegen/src/ir/memflags.rs b/cranelift/codegen/src/ir/memflags.rs
index 8a5626d74..d9066f13a 100644
--- a/cranelift/codegen/src/ir/memflags.rs
+++ b/cranelift/codegen/src/ir/memflags.rs
@@ -2,57 +2,11 @@
 
 use super::TrapCode;
 use core::fmt;
+use core::str::FromStr;
 
 #[cfg(feature = "enable-serde")]
 use serde_derive::{Deserialize, Serialize};
 
-enum FlagBit {
-    /// Guaranteed not to trap. This may enable additional
-    /// optimizations to be performed.
-    Notrap,
-    /// If this memory access traps, use [TrapCode::TableOutOfBounds]
-    /// instead of [TrapCode::HeapOutOfBounds].
-    TableTrap,
-    /// Guaranteed to use "natural alignment" for the given type. This
-    /// may enable better instruction selection.
-    Aligned,
-    /// A load that reads data in memory that does not change for the
-    /// duration of the function's execution. This may enable
-    /// additional optimizations to be performed.
-    Readonly,
-    /// Load multi-byte values from memory in a little-endian format.
-    LittleEndian,
-    /// Load multi-byte values from memory in a big-endian format.
-    BigEndian,
-    /// Accesses only the "heap" part of abstract state. Used for
-    /// alias analysis. Mutually exclusive with "table" and "vmctx".
-    Heap,
-    /// Accesses only the "table" part of abstract state. Used for
-    /// alias analysis. Mutually exclusive with "heap" and "vmctx".
-    Table,
-    /// Accesses only the "vmctx" part of abstract state. Used for
-    /// alias analysis. Mutually exclusive with "heap" and "table".
-    Vmctx,
-    /// Check this load or store for safety when using the
-    /// proof-carrying-code framework. The address must have a
-    /// `PointsTo` fact attached with a sufficiently large valid range
-    /// for the accessed size.
-    Checked,
-}
-
-const NAMES: [&str; 10] = [
-    "notrap",
-    "tabletrap",
-    "aligned",
-    "readonly",
-    "little",
-    "big",
-    "heap",
-    "table",
-    "vmctx",
-    "checked",
-];
-
 /// Endianness of a memory access.
 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
 pub enum Endianness {
@@ -62,6 +16,16 @@ pub enum Endianness {
     Big,
 }
 
+/// Which disjoint region of aliasing memory is accessed in this memory
+/// operation.
+#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
+#[allow(missing_docs)]
+pub enum AliasRegion {
+    Heap,
+    Table,
+    Vmctx,
+}
+
 /// Flags for memory operations like load/store.
 ///
 /// Each of these flags introduce a limited form of undefined behavior. The flags each enable
@@ -75,9 +39,56 @@ pub enum Endianness {
 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
 pub struct MemFlags {
+    // Initialized to all zeros to have all flags have their default value.
+    // This is interpreted through various methods below. Currently the bits of
+    // this are defined as:
+    //
+    // * 0 - aligned flag
+    // * 1 - readonly flag
+    // * 2 - little endian flag
+    // * 3 - big endian flag
+    // * 4 - checked flag
+    // * 5/6 - alias region
+    // * 7/8/9/10 - trap code
+    // * 11/12/13/14/15 - unallocated
+    //
+    // Current properties upheld are:
+    //
+    // * only one of little/big endian is set
+    // * only one alias region can be set - once set it cannot be changed
     bits: u16,
 }
 
+/// Guaranteed to use "natural alignment" for the given type. This
+/// may enable better instruction selection.
+const BIT_ALIGNED: u16 = 1 << 0;
+
+/// A load that reads data in memory that does not change for the
+/// duration of the function's execution. This may enable
+/// additional optimizations to be performed.
+const BIT_READONLY: u16 = 1 << 1;
+
+/// Load multi-byte values from memory in a little-endian format.
+const BIT_LITTLE_ENDIAN: u16 = 1 << 2;
+
+/// Load multi-byte values from memory in a big-endian format.
+const BIT_BIG_ENDIAN: u16 = 1 << 3;
+
+/// Check this load or store for safety when using the
+/// proof-carrying-code framework. The address must have a
+/// `PointsTo` fact attached with a sufficiently large valid range
+/// for the accessed size.
+const BIT_CHECKED: u16 = 1 << 4;
+
+/// Used for alias analysis, indicates which disjoint part of the abstract state
+/// is being accessed.
+const MASK_ALIAS_REGION: u16 = 0b11 << ALIAS_REGION_OFFSET;
+const ALIAS_REGION_OFFSET: u16 = 5;
+
+/// Trap code, if any, for this memory operation.
+const MASK_TRAP_CODE: u16 = 0b1111 << TRAP_CODE_OFFSET;
+const TRAP_CODE_OFFSET: u16 = 7;
+
 impl MemFlags {
     /// Create a new empty set of flags.
     pub const fn new() -> Self {
@@ -91,35 +102,100 @@ impl MemFlags {
     }
 
     /// Read a flag bit.
-    const fn read(self, bit: FlagBit) -> bool {
-        self.bits & (1 << bit as usize) != 0
+    const fn read_bit(self, bit: u16) -> bool {
+        self.bits & bit != 0
     }
 
     /// Return a new `MemFlags` with this flag bit set.
-    const fn with(mut self, bit: FlagBit) -> Self {
-        self.bits |= 1 << bit as usize;
+    const fn with_bit(mut self, bit: u16) -> Self {
+        self.bits |= bit;
+        self
+    }
+
+    /// Reads the alias region that this memory operation works with.
+    pub const fn alias_region(self) -> Option<AliasRegion> {
+        // NB: keep in sync with `with_alias_region`
+        match (self.bits & MASK_ALIAS_REGION) >> ALIAS_REGION_OFFSET {
+            0b00 => None,
+            0b01 => Some(AliasRegion::Heap),
+            0b10 => Some(AliasRegion::Table),
+            0b11 => Some(AliasRegion::Vmctx),
+            _ => unreachable!(),
+        }
+    }
+
+    /// Sets the alias region that this works on to the specified `region`.
+    pub const fn with_alias_region(mut self, region: Option<AliasRegion>) -> Self {
+        // NB: keep in sync with `alias_region`
+        let bits = match region {
+            None => 0b00,
+            Some(AliasRegion::Heap) => 0b01,
+            Some(AliasRegion::Table) => 0b10,
+            Some(AliasRegion::Vmctx) => 0b11,
+        };
+        self.bits &= !MASK_ALIAS_REGION;
+        self.bits |= bits << ALIAS_REGION_OFFSET;
         self
     }
 
+    /// Sets the alias region that this works on to the specified `region`.
+    pub fn set_alias_region(&mut self, region: Option<AliasRegion>) {
+        *self = self.with_alias_region(region);
+    }
+
     /// Set a flag bit by name.
     ///
-    /// Returns true if the flag was found and set, false for an unknown flag name.
-    /// Will also return false when trying to set inconsistent endianness flags.
-    pub fn set_by_name(&mut self, name: &str) -> bool {
-        match NAMES.iter().position(|&s| s == name) {
-            Some(bit) => {
-                let bits = self.bits | 1 << bit;
-                if (bits & (1 << FlagBit::LittleEndian as usize)) != 0
-                    && (bits & (1 << FlagBit::BigEndian as usize)) != 0
-                {
-                    false
-                } else {
-                    self.bits = bits;
-                    true
+    /// Returns true if the flag was found and set, false for an unknown flag
+    /// name.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error message if the `name` is known but couldn't be applied
+    /// due to it being a semantic error.
+    pub fn set_by_name(&mut self, name: &str) -> Result<bool, &'static str> {
+        *self = match name {
+            "notrap" => self.with_trap_code(None),
+            "aligned" => self.with_aligned(),
+            "readonly" => self.with_readonly(),
+            "little" => {
+                if self.read_bit(BIT_BIG_ENDIAN) {
+                    return Err("cannot set both big and little endian bits");
                 }
+                self.with_endianness(Endianness::Little)
             }
-            None => false,
-        }
+            "big" => {
+                if self.read_bit(BIT_LITTLE_ENDIAN) {
+                    return Err("cannot set both big and little endian bits");
+                }
+                self.with_endianness(Endianness::Big)
+            }
+            "heap" => {
+                if self.alias_region().is_some() {
+                    return Err("cannot set more than one alias region");
+                }
+                self.with_alias_region(Some(AliasRegion::Heap))
+            }
+            "table" => {
+                if self.alias_region().is_some() {
+                    return Err("cannot set more than one alias region");
+                }
+                self.with_alias_region(Some(AliasRegion::Table))
+            }
+            "vmctx" => {
+                if self.alias_region().is_some() {
+                    return Err("cannot set more than one alias region");
+                }
+                self.with_alias_region(Some(AliasRegion::Vmctx))
+            }
+            "checked" => self.with_checked(),
+
+            other => match TrapCode::from_str(other) {
+                Ok(TrapCode::User(_)) => return Err("cannot set user trap code on mem flags"),
+                Ok(code) => self.with_trap_code(Some(code)),
+                Err(()) => return Ok(false),
+            },
+        };
+        Ok(true)
     }
 
     /// Return endianness of the memory access.  This will return the endianness
@@ -128,9 +204,9 @@ impl MemFlags {
     /// caller since it is not explicitly encoded in CLIF IR -- this allows a
     /// front end to create IR without having to know the target endianness.
     pub const fn endianness(self, native_endianness: Endianness) -> Endianness {
-        if self.read(FlagBit::LittleEndian) {
+        if self.read_bit(BIT_LITTLE_ENDIAN) {
             Endianness::Little
-        } else if self.read(FlagBit::BigEndian) {
+        } else if self.read_bit(BIT_BIG_ENDIAN) {
             Endianness::Big
         } else {
             native_endianness
@@ -145,53 +221,36 @@ impl MemFlags {
     /// Set endianness of the memory access, returning new flags.
     pub const fn with_endianness(self, endianness: Endianness) -> Self {
         let res = match endianness {
-            Endianness::Little => self.with(FlagBit::LittleEndian),
-            Endianness::Big => self.with(FlagBit::BigEndian),
+            Endianness::Little => self.with_bit(BIT_LITTLE_ENDIAN),
+            Endianness::Big => self.with_bit(BIT_BIG_ENDIAN),
         };
-        assert!(!(res.read(FlagBit::LittleEndian) && res.read(FlagBit::BigEndian)));
+        assert!(!(res.read_bit(BIT_LITTLE_ENDIAN) && res.read_bit(BIT_BIG_ENDIAN)));
         res
     }
 
-    /// Test if the `notrap` flag is set.
+    /// Test if this memory operation cannot trap.
     ///
-    /// Normally, trapping is part of the semantics of a load/store operation. If the platform
-    /// would cause a trap when accessing the effective address, the Cranelift memory operation is
-    /// also required to trap.
+    /// By default `MemFlags` will assume that any load/store can trap and is
+    /// associated with a `TrapCode::HeapOutOfBounds` code. If the trap code is
+    /// configured to `None` though then this method will return `true` and
+    /// indicates that the memory operation will not trap.
     ///
-    /// The `notrap` flag tells Cranelift that the memory is *accessible*, which means that
-    /// accesses will not trap. This makes it possible to delete an unused load or a dead store
-    /// instruction.
+    /// If this returns `true` then the memory is *accessible*, which means
+    /// that accesses will not trap. This makes it possible to delete an unused
+    /// load or a dead store instruction.
     pub const fn notrap(self) -> bool {
-        self.read(FlagBit::Notrap)
+        self.trap_code().is_none()
     }
 
-    /// Set the `notrap` flag.
+    /// Sets the trap code for this `MemFlags` to `None`.
     pub fn set_notrap(&mut self) {
         *self = self.with_notrap();
     }
 
-    /// Set the `notrap` flag, returning new flags.
+    /// Sets the trap code for this `MemFlags` to `None`, returning the new
+    /// flags.
     pub const fn with_notrap(self) -> Self {
-        self.with(FlagBit::Notrap)
-    }
-
-    /// Test if the `tabletrap` bit is set.
-    ///
-    /// By default, if this memory access traps, the associated trap
-    /// code will be `HeapOutOfBounds`. With this flag set, the trap
-    /// code will instead be `TableOutOfBounds`.
-    pub const fn tabletrap(self) -> bool {
-        self.read(FlagBit::TableTrap)
-    }
-
-    /// Set the `tabletrap` bit.
-    pub fn set_tabletrap(&mut self) {
-        *self = self.with_tabletrap();
-    }
-
-    /// Set the `tabletrap` bit, returning new flags.
-    pub const fn with_tabletrap(self) -> Self {
-        self.with(FlagBit::TableTrap)
+        self.with_trap_code(None)
     }
 
     /// Test if the `aligned` flag is set.
@@ -200,7 +259,7 @@ impl MemFlags {
     /// `aligned` flag is set, the instruction is permitted to trap or return a wrong result if the
     /// effective address is misaligned.
     pub const fn aligned(self) -> bool {
-        self.read(FlagBit::Aligned)
+        self.read_bit(BIT_ALIGNED)
     }
 
     /// Set the `aligned` flag.
@@ -210,7 +269,7 @@ impl MemFlags {
 
     /// Set the `aligned` flag, returning new flags.
     pub const fn with_aligned(self) -> Self {
-        self.with(FlagBit::Aligned)
+        self.with_bit(BIT_ALIGNED)
     }
 
     /// Test if the `readonly` flag is set.
@@ -219,7 +278,7 @@ impl MemFlags {
     /// This results in undefined behavior if the dereferenced memory is mutated at any time
     /// between when the function is called and when it is exited.
     pub const fn readonly(self) -> bool {
-        self.read(FlagBit::Readonly)
+        self.read_bit(BIT_READONLY)
     }
 
     /// Set the `readonly` flag.
@@ -229,79 +288,7 @@ impl MemFlags {
 
     /// Set the `readonly` flag, returning new flags.
     pub const fn with_readonly(self) -> Self {
-        self.with(FlagBit::Readonly)
-    }
-
-    /// Test if the `heap` bit is set.
-    ///
-    /// Loads and stores with this flag accesses the "heap" part of
-    /// abstract state. This is disjoint from the "table", "vmctx",
-    /// and "other" parts of abstract state. In concrete terms, this
-    /// means that behavior is undefined if the same memory is also
-    /// accessed by another load/store with one of the other
-    /// alias-analysis bits (`table`, `vmctx`) set, or `heap` not set.
-    pub const fn heap(self) -> bool {
-        self.read(FlagBit::Heap)
-    }
-
-    /// Set the `heap` bit. See the notes about mutual exclusion with
-    /// other bits in `heap()`.
-    pub fn set_heap(&mut self) {
-        *self = self.with_heap();
-    }
-
-    /// Set the `heap` bit, returning new flags.
-    pub const fn with_heap(self) -> Self {
-        assert!(!self.table() && !self.vmctx());
-        self.with(FlagBit::Heap)
-    }
-
-    /// Test if the `table` bit is set.
-    ///
-    /// Loads and stores with this flag accesses the "table" part of
-    /// abstract state. This is disjoint from the "heap", "vmctx",
-    /// and "other" parts of abstract state. In concrete terms, this
-    /// means that behavior is undefined if the same memory is also
-    /// accessed by another load/store with one of the other
-    /// alias-analysis bits (`heap`, `vmctx`) set, or `table` not set.
-    pub const fn table(self) -> bool {
-        self.read(FlagBit::Table)
-    }
-
-    /// Set the `table` bit. See the notes about mutual exclusion with
-    /// other bits in `table()`.
-    pub fn set_table(&mut self) {
-        *self = self.with_table();
-    }
-
-    /// Set the `table` bit, returning new flags.
-    pub const fn with_table(self) -> Self {
-        assert!(!self.heap() && !self.vmctx());
-        self.with(FlagBit::Table)
-    }
-
-    /// Test if the `vmctx` bit is set.
-    ///
-    /// Loads and stores with this flag accesses the "vmctx" part of
-    /// abstract state. This is disjoint from the "heap", "table",
-    /// and "other" parts of abstract state. In concrete terms, this
-    /// means that behavior is undefined if the same memory is also
-    /// accessed by another load/store with one of the other
-    /// alias-analysis bits (`heap`, `table`) set, or `vmctx` not set.
-    pub const fn vmctx(self) -> bool {
-        self.read(FlagBit::Vmctx)
-    }
-
-    /// Set the `vmctx` bit. See the notes about mutual exclusion with
-    /// other bits in `vmctx()`.
-    pub fn set_vmctx(&mut self) {
-        *self = self.with_vmctx();
-    }
-
-    /// Set the `vmctx` bit, returning new flags.
-    pub const fn with_vmctx(self) -> Self {
-        assert!(!self.heap() && !self.table());
-        self.with(FlagBit::Vmctx)
+        self.with_bit(BIT_READONLY)
     }
 
     /// Test if the `checked` bit is set.
@@ -316,7 +303,7 @@ impl MemFlags {
     /// checker's correctness) to access valid memory. This can be
     /// used to ensure memory safety and sandboxing.
     pub const fn checked(self) -> bool {
-        self.read(FlagBit::Checked)
+        self.read_bit(BIT_CHECKED)
     }
 
     /// Set the `checked` bit.
@@ -326,32 +313,139 @@ impl MemFlags {
 
     /// Set the `checked` bit, returning new flags.
     pub const fn with_checked(self) -> Self {
-        self.with(FlagBit::Checked)
+        self.with_bit(BIT_CHECKED)
     }
 
-    /// Get the trap code to report if this memory access traps. If the
-    /// `notrap` flag is set, then return `None`. Otherwise, the choice
-    /// of trap code depends on the `tabletrap` flag. The default trap
-    /// code is `HeapOutOfBounds`, but with `tabletrap` set, the trap
-    /// code is instead `TableOutOfBounds`.
+    /// Get the trap code to report if this memory access traps.
+    ///
+    /// A `None` trap code indicates that this memory access does not trap.
     pub const fn trap_code(self) -> Option<TrapCode> {
-        if self.notrap() {
-            None
-        } else if self.tabletrap() {
-            Some(TrapCode::TableOutOfBounds)
-        } else {
-            Some(TrapCode::HeapOutOfBounds)
+        // NB: keep this encoding in sync with `with_trap_code` below.
+        //
+        // Also note that the default, all zeros, is `HeapOutOfBounds`. It is
+        // intentionally not `None` so memory operations are all considered
+        // effect-ful by default.
+        match (self.bits & MASK_TRAP_CODE) >> TRAP_CODE_OFFSET {
+            0b0000 => Some(TrapCode::HeapOutOfBounds),
+            0b0001 => Some(TrapCode::StackOverflow),
+            0b0010 => Some(TrapCode::HeapMisaligned),
+            0b0011 => Some(TrapCode::TableOutOfBounds),
+            0b0100 => Some(TrapCode::IndirectCallToNull),
+            0b0101 => Some(TrapCode::BadSignature),
+            0b0110 => Some(TrapCode::IntegerOverflow),
+            0b0111 => Some(TrapCode::IntegerDivisionByZero),
+            0b1000 => Some(TrapCode::BadConversionToInteger),
+            0b1001 => Some(TrapCode::UnreachableCodeReached),
+            0b1010 => Some(TrapCode::Interrupt),
+            0b1011 => Some(TrapCode::NullReference),
+            // 0b1100 => {} not allocated
+            // 0b1101 => {} not allocated
+            // 0b1110 => {} not allocated
+            0b1111 => None,
+            _ => unreachable!(),
         }
     }
+
+    /// Configures these flags with the specified trap code `code`.
+    ///
+    /// Note that `TrapCode::User(_)` cannot be set in `MemFlags`. A trap code
+    /// indicates that this memory operation cannot be optimized away and it
+    /// must "stay where it is" in the programs. Traps are considered side
+    /// effects, for example, and have meaning through the trap code that is
+    /// communicated and which instruction trapped.
+    pub const fn with_trap_code(mut self, code: Option<TrapCode>) -> Self {
+        let bits = match code {
+            Some(TrapCode::HeapOutOfBounds) => 0b0000,
+            Some(TrapCode::StackOverflow) => 0b0001,
+            Some(TrapCode::HeapMisaligned) => 0b0010,
+            Some(TrapCode::TableOutOfBounds) => 0b0011,
+            Some(TrapCode::IndirectCallToNull) => 0b0100,
+            Some(TrapCode::BadSignature) => 0b0101,
+            Some(TrapCode::IntegerOverflow) => 0b0110,
+            Some(TrapCode::IntegerDivisionByZero) => 0b0111,
+            Some(TrapCode::BadConversionToInteger) => 0b1000,
+            Some(TrapCode::UnreachableCodeReached) => 0b1001,
+            Some(TrapCode::Interrupt) => 0b1010,
+            Some(TrapCode::NullReference) => 0b1011,
+            None => 0b1111,
+
+            Some(TrapCode::User(_)) => panic!("cannot set user trap code in mem flags"),
+        };
+        self.bits &= !MASK_TRAP_CODE;
+        self.bits |= bits << TRAP_CODE_OFFSET;
+        self
+    }
 }
 
 impl fmt::Display for MemFlags {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        for (i, n) in NAMES.iter().enumerate() {
-            if self.bits & (1 << i) != 0 {
-                write!(f, " {}", n)?;
-            }
+        match self.trap_code() {
+            None => write!(f, " notrap")?,
+            // This is the default trap code, so don't print anything extra
+            // for this.
+            Some(TrapCode::HeapOutOfBounds) => {}
+            Some(t) => write!(f, " {t}")?,
+        }
+        if self.aligned() {
+            write!(f, " aligned")?;
+        }
+        if self.readonly() {
+            write!(f, " readonly")?;
+        }
+        if self.read_bit(BIT_BIG_ENDIAN) {
+            write!(f, " big")?;
+        }
+        if self.read_bit(BIT_LITTLE_ENDIAN) {
+            write!(f, " little")?;
+        }
+        if self.checked() {
+            write!(f, " checked")?;
+        }
+        match self.alias_region() {
+            None => {}
+            Some(AliasRegion::Heap) => write!(f, " heap")?,
+            Some(AliasRegion::Table) => write!(f, " table")?,
+            Some(AliasRegion::Vmctx) => write!(f, " vmctx")?,
         }
         Ok(())
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn roundtrip_traps() {
+        for trap in TrapCode::non_user_traps().iter().copied() {
+            let flags = MemFlags::new().with_trap_code(Some(trap));
+            assert_eq!(flags.trap_code(), Some(trap));
+        }
+        let flags = MemFlags::new().with_trap_code(None);
+        assert_eq!(flags.trap_code(), None);
+    }
+
+    #[test]
+    fn cannot_set_big_and_little() {
+        let mut big = MemFlags::new().with_endianness(Endianness::Big);
+        assert!(big.set_by_name("little").is_err());
+
+        let mut little = MemFlags::new().with_endianness(Endianness::Little);
+        assert!(little.set_by_name("big").is_err());
+    }
+
+    #[test]
+    fn only_one_region() {
+        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Heap));
+        assert!(big.set_by_name("table").is_err());
+        assert!(big.set_by_name("vmctx").is_err());
+
+        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Table));
+        assert!(big.set_by_name("heap").is_err());
+        assert!(big.set_by_name("vmctx").is_err());
+
+        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Vmctx));
+        assert!(big.set_by_name("heap").is_err());
+        assert!(big.set_by_name("table").is_err());
+    }
+}
diff --git a/cranelift/codegen/src/ir/mod.rs b/cranelift/codegen/src/ir/mod.rs
index aff5078a7..445496e5e 100644
--- a/cranelift/codegen/src/ir/mod.rs
+++ b/cranelift/codegen/src/ir/mod.rs
@@ -53,7 +53,7 @@ pub use crate::ir::jumptable::JumpTableData;
 pub use crate::ir::known_symbol::KnownSymbol;
 pub use crate::ir::layout::Layout;
 pub use crate::ir::libcall::{get_probestack_funcref, LibCall};
-pub use crate::ir::memflags::{Endianness, MemFlags};
+pub use crate::ir::memflags::{AliasRegion, Endianness, MemFlags};
 pub use crate::ir::memtype::{MemoryTypeData, MemoryTypeField};
 pub use crate::ir::pcc::{BaseExpr, Expr, Fact, FactContext, PccError, PccResult};
 pub use crate::ir::progpoint::ProgramPoint;
diff --git a/cranelift/fuzzgen/src/function_generator.rs b/cranelift/fuzzgen/src/function_generator.rs
index 6dc17eac1..4e2506bdb 100644
--- a/cranelift/fuzzgen/src/function_generator.rs
+++ b/cranelift/fuzzgen/src/function_generator.rs
@@ -9,8 +9,8 @@ use cranelift::codegen::ir::instructions::{InstructionFormat, ResolvedConstraint
 use cranelift::codegen::ir::stackslot::StackSize;
 
 use cranelift::codegen::ir::{
-    types::*, AtomicRmwOp, Block, ConstantData, Endianness, ExternalName, FuncRef, Function,
-    LibCall, Opcode, SigRef, Signature, StackSlot, UserExternalName, UserFuncName, Value,
+    types::*, AliasRegion, AtomicRmwOp, Block, ConstantData, Endianness, ExternalName, FuncRef,
+    Function, LibCall, Opcode, SigRef, Signature, StackSlot, UserExternalName, UserFuncName, Value,
 };
 use cranelift::codegen::isa::CallConv;
 use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext, Switch, Variable};
@@ -1176,12 +1176,12 @@ impl AACategory {
     }
 
     pub fn update_memflags(&self, flags: &mut MemFlags) {
-        match self {
-            AACategory::Other => {}
-            AACategory::Heap => flags.set_heap(),
-            AACategory::Table => flags.set_table(),
-            AACategory::VmCtx => flags.set_vmctx(),
-        }
+        flags.set_alias_region(match self {
+            AACategory::Other => None,
+            AACategory::Heap => Some(AliasRegion::Heap),
+            AACategory::Table => Some(AliasRegion::Table),
+            AACategory::VmCtx => Some(AliasRegion::Vmctx),
+        })
     }
 }
 
diff --git a/cranelift/reader/src/parser.rs b/cranelift/reader/src/parser.rs
index 03c29fa4a..0dc9289ef 100644
--- a/cranelift/reader/src/parser.rs
+++ b/cranelift/reader/src/parser.rs
@@ -887,16 +887,18 @@ impl<'a> Parser<'a> {
     }
 
     // Match and a consume a possibly empty sequence of memory operation flags.
-    fn optional_memflags(&mut self) -> MemFlags {
+    fn optional_memflags(&mut self) -> ParseResult<MemFlags> {
         let mut flags = MemFlags::new();
         while let Some(Token::Identifier(text)) = self.token() {
-            if flags.set_by_name(text) {
-                self.consume();
-            } else {
-                break;
+            match flags.set_by_name(text) {
+                Ok(true) => {
+                    self.consume();
+                }
+                Ok(false) => break,
+                Err(msg) => return err!(self.loc, msg),
             }
         }
-        flags
+        Ok(flags)
     }
 
     // Match and consume an identifier.
@@ -1570,7 +1572,7 @@ impl<'a> Parser<'a> {
                     "expected '.' followed by type in load global value decl",
                 )?;
                 let global_type = self.match_type("expected load type")?;
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let base = self.match_gv("expected global value: gv«n»")?;
                 let offset = self.optional_offset32()?;
 
@@ -3036,7 +3038,7 @@ impl<'a> Parser<'a> {
                 }
             }
             InstructionFormat::Load => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let addr = self.match_value("expected SSA value address")?;
                 let offset = self.optional_offset32()?;
                 InstructionData::Load {
@@ -3047,7 +3049,7 @@ impl<'a> Parser<'a> {
                 }
             }
             InstructionFormat::Store => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let arg = self.match_value("expected SSA value operand")?;
                 self.match_token(Token::Comma, "expected ',' between operands")?;
                 let addr = self.match_value("expected SSA value address")?;
@@ -3070,7 +3072,7 @@ impl<'a> Parser<'a> {
                 InstructionData::CondTrap { opcode, arg, code }
             }
             InstructionFormat::AtomicCas => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let addr = self.match_value("expected SSA value address")?;
                 self.match_token(Token::Comma, "expected ',' between operands")?;
                 let expected = self.match_value("expected SSA value address")?;
@@ -3083,7 +3085,7 @@ impl<'a> Parser<'a> {
                 }
             }
             InstructionFormat::AtomicRmw => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let op = self.match_enum("expected AtomicRmwOp")?;
                 let addr = self.match_value("expected SSA value address")?;
                 self.match_token(Token::Comma, "expected ',' between operands")?;
@@ -3096,7 +3098,7 @@ impl<'a> Parser<'a> {
                 }
             }
             InstructionFormat::LoadNoOffset => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let addr = self.match_value("expected SSA value address")?;
                 InstructionData::LoadNoOffset {
                     opcode,
@@ -3105,7 +3107,7 @@ impl<'a> Parser<'a> {
                 }
             }
             InstructionFormat::StoreNoOffset => {
-                let flags = self.optional_memflags();
+                let flags = self.optional_memflags()?;
                 let arg = self.match_value("expected SSA value operand")?;
                 self.match_token(Token::Comma, "expected ',' between operands")?;
                 let addr = self.match_value("expected SSA value address")?;
diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs
index f4a284d5e..bb3d6c21f 100644
--- a/cranelift/wasm/src/code_translator.rs
+++ b/cranelift/wasm/src/code_translator.rs
@@ -177,7 +177,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
                     let addr = builder.ins().global_value(environ.pointer_type(), gv);
                     let mut flags = ir::MemFlags::trusted();
                     // Put globals in the "table" abstract heap category as well.
-                    flags.set_table();
+                    flags.set_alias_region(Some(ir::AliasRegion::Table));
                     builder.ins().load(ty, flags, addr, offset)
                 }
                 GlobalVariable::Custom => environ.translate_custom_global_get(
@@ -194,7 +194,7 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
                     let addr = builder.ins().global_value(environ.pointer_type(), gv);
                     let mut flags = ir::MemFlags::trusted();
                     // Put globals in the "table" abstract heap category as well.
-                    flags.set_table();
+                    flags.set_alias_region(Some(ir::AliasRegion::Table));
                     let mut val = state.pop1();
                     // Ensure SIMD values are cast to their default Cranelift type, I8x16.
                     if ty.is_vector() {
@@ -2837,7 +2837,7 @@ where
     // state. This may allow alias analysis to merge redundant loads,
     // etc. when heap accesses occur interleaved with other (table,
     // vmctx, stack) accesses.
-    flags.set_heap();
+    flags.set_alias_region(Some(ir::AliasRegion::Heap));
 
     Ok(Reachability::Reachable((flags, index, addr)))
 }
diff --git a/cranelift/wasm/src/environ/dummy.rs b/cranelift/wasm/src/environ/dummy.rs
index a42a40384..dac12cb1b 100644
--- a/cranelift/wasm/src/environ/dummy.rs
+++ b/cranelift/wasm/src/environ/dummy.rs
@@ -529,7 +529,7 @@ impl<'dummy_environment> FuncEnvironment for DummyFuncEnvironment<'dummy_environ
         self.ensure_table_exists(builder.func, table_index);
         let table = self.tables[table_index].as_ref().unwrap();
         let table_entry_addr = table.prepare_table_addr(builder, index, pointer_type, true);
-        let flags = ir::MemFlags::trusted().with_table();
+        let flags = ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
         let value = builder
             .ins()
             .load(self.reference_type(), flags, table_entry_addr, 0);
@@ -547,7 +547,7 @@ impl<'dummy_environment> FuncEnvironment for DummyFuncEnvironment<'dummy_environ
         self.ensure_table_exists(builder.func, table_index);
         let table = self.tables[table_index].as_ref().unwrap();
         let table_entry_addr = table.prepare_table_addr(builder, index, pointer_type, true);
-        let flags = ir::MemFlags::trusted().with_table();
+        let flags = ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
         builder.ins().store(flags, value, table_entry_addr, 0);
         Ok(())
     }
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index b093e666f..694d8a173 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -961,7 +961,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
             pointer_type,
             self.isa.flags().enable_table_access_spectre_mitigation(),
         );
-        let flags = ir::MemFlags::trusted().with_table();
+        let flags = ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
         let value = builder.ins().load(pointer_type, flags, table_entry_addr, 0);
         // Mask off the "initialized bit". See documentation on
         // FUNCREF_INIT_BIT in crates/environ/src/ref_bits.rs for more
@@ -1446,7 +1446,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                     pointer_type,
                     self.isa.flags().enable_table_access_spectre_mitigation(),
                 );
-                let flags = ir::MemFlags::trusted().with_table();
+                let flags = ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
                 let elem = builder.ins().load(reference_type, flags, elem_addr, 0);
 
                 let elem_is_null = builder.ins().is_null(elem);
@@ -1563,7 +1563,8 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                         let value_with_init_bit = builder
                             .ins()
                             .bor_imm(value, Imm64::from(FUNCREF_INIT_BIT as i64));
-                        let flags = ir::MemFlags::trusted().with_table();
+                        let flags =
+                            ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
                         builder
                             .ins()
                             .store(flags, value_with_init_bit, table_entry_addr, 0);
@@ -1633,7 +1634,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                     pointer_type,
                     self.isa.flags().enable_table_access_spectre_mitigation(),
                 );
-                let flags = ir::MemFlags::trusted().with_table();
+                let flags = ir::MemFlags::trusted().with_alias_region(Some(ir::AliasRegion::Table));
                 let current_elem = builder.ins().load(pointer_type, flags, table_entry_addr, 0);
                 builder.ins().store(flags, value, table_entry_addr, 0);
 


Test patch:
diff --git a/tests/disas/typed-funcrefs.wat b/tests/disas/typed-funcrefs.wat
index 2259d7f3b..0e69d7373 100644
--- a/tests/disas/typed-funcrefs.wat
+++ b/tests/disas/typed-funcrefs.wat
@@ -14,9 +14,9 @@
   (type $ic-stub (func (param i32 i32 i32 i32) (result i32)))
 
   ;; This syntax declares a table that is exactly 100 elements, whose
-  ;; elements are non-nullable function references, and whose default
-  ;; value (needed because non-nullable) is a pointer to `$ic1`.
-  (table $ic-sites 100 100 (ref $ic-stub) (ref.func $ic1))
+  ;; elements are nullable function references, and whose default
+  ;; value is `null`.
+  (table $ic-sites 100 100 (ref null $ic-stub))
 
   (func $ic1 (param i32 i32 i32 i32) (result i32)
         local.get 0)
@@ -59,10 +59,10 @@
 ;;
 ;;                                 block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32):
 ;;                                     v6 -> v2
-;; @002c                               jump block1
+;; @0027                               jump block1
 ;;
 ;;                                 block1:
-;; @002c                               return v2
+;; @0027                               return v2
 ;; }
 ;;
 ;; function u0:1(i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast {
@@ -87,80 +87,80 @@
 ;;                                     v32 -> v4
 ;;                                     v33 -> v5
 ;;                                     v62 = iconst.i8 0
-;; @003b                               brif v62, block6, block7  ; v62 = 0
+;; @0036                               brif v62, block6, block7  ; v62 = 0
 ;;
 ;;                                 block6 cold:
-;; @003b                               trap table_oob
+;; @0036                               trap table_oob
 ;;
 ;;                                 block7:
-;; @003b                               v12 = load.i64 notrap aligned v0+72
+;; @0036                               v12 = load.i64 notrap aligned v0+72
 ;;                                     v79 = iconst.i8 0
 ;;                                     v70 = iconst.i64 8
-;; @003b                               v14 = iadd v12, v70  ; v70 = 8
-;; @003b                               v16 = select_spectre_guard v79, v12, v14  ; v79 = 0
-;; @003b                               v17 = load.i64 notrap aligned table v16
+;; @0036                               v14 = iadd v12, v70  ; v70 = 8
+;; @0036                               v16 = select_spectre_guard v79, v12, v14  ; v79 = 0
+;; @0036                               v17 = load.i64 notrap aligned table v16
 ;;                                     v58 = iconst.i64 -2
-;; @003b                               v18 = band v17, v58  ; v58 = -2
-;; @003b                               brif v17, block3(v18), block2
+;; @0036                               v18 = band v17, v58  ; v58 = -2
+;; @0036                               brif v17, block3(v18), block2
 ;;
 ;;                                 block2 cold:
-;; @004e                               v48 = load.i64 notrap aligned readonly v0+56
-;; @004e                               v49 = load.i64 notrap aligned readonly v48+72
-;; @002f                               v7 = iconst.i32 0
+;; @0049                               v48 = load.i64 notrap aligned readonly v0+56
+;; @0049                               v49 = load.i64 notrap aligned readonly v48+72
+;; @002a                               v7 = iconst.i32 0
 ;;                                     v28 -> v7
-;; @0039                               v8 = iconst.i32 1
-;; @003b                               v24 = call_indirect sig0, v49(v0, v7, v8)  ; v7 = 0, v8 = 1
-;; @003b                               jump block3(v24)
+;; @0034                               v8 = iconst.i32 1
+;; @0036                               v24 = call_indirect sig0, v49(v0, v7, v8)  ; v7 = 0, v8 = 1
+;; @0036                               jump block3(v24)
 ;;
 ;;                                 block3(v19: i64):
-;; @003d                               brif v19, block9, block8
+;; @0038                               brif v19, block9, block8
 ;;
 ;;                                 block8 cold:
-;; @003d                               trap null_reference
+;; @0038                               trap null_reference
 ;;
 ;;                                 block9:
-;; @003d                               v25 = load.i64 notrap aligned readonly v19+16
-;; @003d                               v26 = load.i64 notrap aligned readonly v19+32
-;; @003d                               v27 = call_indirect sig1, v25(v26, v0, v2, v3, v4, v5)
+;; @0038                               v25 = load.i64 notrap aligned readonly v19+16
+;; @0038                               v26 = load.i64 notrap aligned readonly v19+32
+;; @0038                               v27 = call_indirect sig1, v25(v26, v0, v2, v3, v4, v5)
 ;;                                     v80 = iconst.i8 0
-;; @004e                               brif v80, block10, block11  ; v80 = 0
+;; @0049                               brif v80, block10, block11  ; v80 = 0
 ;;
 ;;                                 block10 cold:
-;; @004e                               trap table_oob
+;; @0049                               trap table_oob
 ;;
 ;;                                 block11:
-;; @004e                               v38 = load.i64 notrap aligned v0+72
+;; @0049                               v38 = load.i64 notrap aligned v0+72
 ;;                                     v81 = iconst.i8 0
 ;;                                     v78 = iconst.i64 16
-;; @004e                               v40 = iadd v38, v78  ; v78 = 16
-;; @004e                               v42 = select_spectre_guard v81, v38, v40  ; v81 = 0
-;; @004e                               v43 = load.i64 notrap aligned table v42
+;; @0049                               v40 = iadd v38, v78  ; v78 = 16
+;; @0049                               v42 = select_spectre_guard v81, v38, v40  ; v81 = 0
+;; @0049                               v43 = load.i64 notrap aligned table v42
 ;;                                     v82 = iconst.i64 -2
 ;;                                     v83 = band v43, v82  ; v82 = -2
-;; @004e                               brif v43, block5(v83), block4
+;; @0049                               brif v43, block5(v83), block4
 ;;
 ;;                                 block4 cold:
 ;;                                     v84 = load.i64 notrap aligned readonly v0+56
 ;;                                     v85 = load.i64 notrap aligned readonly v84+72
 ;;                                     v86 = iconst.i32 0
-;; @004c                               v34 = iconst.i32 2
-;; @004e                               v50 = call_indirect sig0, v85(v0, v86, v34)  ; v86 = 0, v34 = 2
-;; @004e                               jump block5(v50)
+;; @0047                               v34 = iconst.i32 2
+;; @0049                               v50 = call_indirect sig0, v85(v0, v86, v34)  ; v86 = 0, v34 = 2
+;; @0049                               jump block5(v50)
 ;;
 ;;                                 block5(v45: i64):
-;; @0050                               brif v45, block13, block12
+;; @004b                               brif v45, block13, block12
 ;;
 ;;                                 block12 cold:
-;; @0050                               trap null_reference
+;; @004b                               trap null_reference
 ;;
 ;;                                 block13:
-;; @0050                               v51 = load.i64 notrap aligned readonly v45+16
-;; @0050                               v52 = load.i64 notrap aligned readonly v45+32
-;; @0050                               v53 = call_indirect sig1, v51(v52, v0, v2, v3, v4, v5)
-;; @0059                               jump block1
+;; @004b                               v51 = load.i64 notrap aligned readonly v45+16
+;; @004b                               v52 = load.i64 notrap aligned readonly v45+32
+;; @004b                               v53 = call_indirect sig1, v51(v52, v0, v2, v3, v4, v5)
+;; @0054                               jump block1
 ;;
 ;;                                 block1:
-;; @0054                               v55 = iadd.i32 v53, v27
+;; @004f                               v55 = iadd.i32 v53, v27
 ;;                                     v6 -> v55
-;; @0059                               return v55
+;; @0054                               return v55
 ;; }