Skip to main content

cranelift_codegen/ir/
memflags.rs

1//! Memory operation flags.
2
3use super::TrapCode;
4use core::fmt;
5use core::num::NonZeroU8;
6use core::str::FromStr;
7
8#[cfg(feature = "enable-serde")]
9use serde_derive::{Deserialize, Serialize};
10
11/// Endianness of a memory access.
12#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
13pub enum Endianness {
14    /// Little-endian
15    Little,
16    /// Big-endian
17    Big,
18}
19
20/// Which disjoint region of aliasing memory is accessed in this memory
21/// operation.
22#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23#[repr(u8)]
24#[expect(missing_docs, reason = "self-describing variants")]
25#[rustfmt::skip]
26pub enum AliasRegion {
27    // None = 0b00;
28    Heap    = 0b01,
29    Table   = 0b10,
30    Vmctx   = 0b11,
31}
32
33impl AliasRegion {
34    const fn from_bits(bits: u8) -> Option<Self> {
35        match bits {
36            0b00 => None,
37            0b01 => Some(Self::Heap),
38            0b10 => Some(Self::Table),
39            0b11 => Some(Self::Vmctx),
40            _ => panic!("invalid alias region bits"),
41        }
42    }
43
44    const fn to_bits(region: Option<Self>) -> u8 {
45        match region {
46            None => 0b00,
47            Some(r) => r as u8,
48        }
49    }
50}
51
52/// Flags for memory operations like load/store.
53///
54/// Each of these flags introduce a limited form of undefined behavior. The flags each enable
55/// certain optimizations that need to make additional assumptions. Generally, the semantics of a
56/// program does not change when a flag is removed, but adding a flag will.
57///
58/// In addition, the flags determine the endianness of the memory access.  By default,
59/// any memory access uses the native endianness determined by the target ISA.  This can
60/// be overridden for individual accesses by explicitly specifying little- or big-endian
61/// semantics via the flags.
62#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
63#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
64pub struct MemFlags {
65    // Initialized to all zeros to have all flags have their default value.
66    // This is interpreted through various methods below. Currently the bits of
67    // this are defined as:
68    //
69    // * 0 - aligned flag
70    // * 1 - readonly flag
71    // * 2 - little endian flag
72    // * 3 - big endian flag
73    // * 4 - checked flag
74    // * 5/6 - alias region
75    // * 7/8/9/10/11/12/13/14 - trap code
76    // * 15 - can_move flag
77    //
78    // Current properties upheld are:
79    //
80    // * only one of little/big endian is set
81    // * only one alias region can be set - once set it cannot be changed
82    bits: u16,
83}
84
85/// Guaranteed to use "natural alignment" for the given type. This
86/// may enable better instruction selection.
87const BIT_ALIGNED: u16 = 1 << 0;
88
89/// A load that reads data in memory that does not change for the
90/// duration of the function's execution. This may enable
91/// additional optimizations to be performed.
92const BIT_READONLY: u16 = 1 << 1;
93
94/// Load multi-byte values from memory in a little-endian format.
95const BIT_LITTLE_ENDIAN: u16 = 1 << 2;
96
97/// Load multi-byte values from memory in a big-endian format.
98const BIT_BIG_ENDIAN: u16 = 1 << 3;
99
100/// Used for alias analysis, indicates which disjoint part of the abstract state
101/// is being accessed.
102const MASK_ALIAS_REGION: u16 = 0b11 << ALIAS_REGION_OFFSET;
103const ALIAS_REGION_OFFSET: u16 = 5;
104
105/// Trap code, if any, for this memory operation.
106const MASK_TRAP_CODE: u16 = 0b1111_1111 << TRAP_CODE_OFFSET;
107const TRAP_CODE_OFFSET: u16 = 7;
108
109/// Whether this memory operation may be freely moved by the optimizer so long
110/// as its data dependencies are satisfied. That is, by setting this flag, the
111/// producer is guaranteeing that this memory operation's safety is not guarded
112/// by outside-the-data-flow-graph properties, like implicit bounds-checking
113/// control dependencies.
114const BIT_CAN_MOVE: u16 = 1 << 15;
115
116impl MemFlags {
117    /// Create a new empty set of flags.
118    pub const fn new() -> Self {
119        Self { bits: 0 }.with_trap_code(Some(TrapCode::HEAP_OUT_OF_BOUNDS))
120    }
121
122    /// Create a set of flags representing an access from a "trusted" address, meaning it's
123    /// known to be aligned and non-trapping.
124    pub const fn trusted() -> Self {
125        Self::new().with_notrap().with_aligned()
126    }
127
128    /// Read a flag bit.
129    const fn read_bit(self, bit: u16) -> bool {
130        self.bits & bit != 0
131    }
132
133    /// Return a new `MemFlags` with this flag bit set.
134    const fn with_bit(mut self, bit: u16) -> Self {
135        self.bits |= bit;
136        self
137    }
138
139    /// Reads the alias region that this memory operation works with.
140    pub const fn alias_region(self) -> Option<AliasRegion> {
141        AliasRegion::from_bits(((self.bits & MASK_ALIAS_REGION) >> ALIAS_REGION_OFFSET) as u8)
142    }
143
144    /// Sets the alias region that this works on to the specified `region`.
145    pub const fn with_alias_region(mut self, region: Option<AliasRegion>) -> Self {
146        let bits = AliasRegion::to_bits(region);
147        self.bits &= !MASK_ALIAS_REGION;
148        self.bits |= (bits as u16) << ALIAS_REGION_OFFSET;
149        self
150    }
151
152    /// Sets the alias region that this works on to the specified `region`.
153    pub fn set_alias_region(&mut self, region: Option<AliasRegion>) {
154        *self = self.with_alias_region(region);
155    }
156
157    /// Set a flag bit by name.
158    ///
159    /// Returns true if the flag was found and set, false for an unknown flag
160    /// name.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error message if the `name` is known but couldn't be applied
165    /// due to it being a semantic error.
166    pub fn set_by_name(&mut self, name: &str) -> Result<bool, &'static str> {
167        *self = match name {
168            "notrap" => self.with_trap_code(None),
169            "aligned" => self.with_aligned(),
170            "readonly" => self.with_readonly(),
171            "little" => {
172                if self.read_bit(BIT_BIG_ENDIAN) {
173                    return Err("cannot set both big and little endian bits");
174                }
175                self.with_endianness(Endianness::Little)
176            }
177            "big" => {
178                if self.read_bit(BIT_LITTLE_ENDIAN) {
179                    return Err("cannot set both big and little endian bits");
180                }
181                self.with_endianness(Endianness::Big)
182            }
183            "heap" => {
184                if self.alias_region().is_some() {
185                    return Err("cannot set more than one alias region");
186                }
187                self.with_alias_region(Some(AliasRegion::Heap))
188            }
189            "table" => {
190                if self.alias_region().is_some() {
191                    return Err("cannot set more than one alias region");
192                }
193                self.with_alias_region(Some(AliasRegion::Table))
194            }
195            "vmctx" => {
196                if self.alias_region().is_some() {
197                    return Err("cannot set more than one alias region");
198                }
199                self.with_alias_region(Some(AliasRegion::Vmctx))
200            }
201            "can_move" => self.with_can_move(),
202
203            other => match TrapCode::from_str(other) {
204                Ok(code) => self.with_trap_code(Some(code)),
205                Err(()) => return Ok(false),
206            },
207        };
208        Ok(true)
209    }
210
211    /// Return endianness of the memory access.  This will return the endianness
212    /// explicitly specified by the flags if any, and will default to the native
213    /// endianness otherwise.  The native endianness has to be provided by the
214    /// caller since it is not explicitly encoded in CLIF IR -- this allows a
215    /// front end to create IR without having to know the target endianness.
216    pub const fn endianness(self, native_endianness: Endianness) -> Endianness {
217        if self.read_bit(BIT_LITTLE_ENDIAN) {
218            Endianness::Little
219        } else if self.read_bit(BIT_BIG_ENDIAN) {
220            Endianness::Big
221        } else {
222            native_endianness
223        }
224    }
225
226    /// Return endianness of the memory access, if explicitly specified.
227    ///
228    /// If the endianness is not explicitly specified, this will return `None`,
229    /// which means "native endianness".
230    pub const fn explicit_endianness(self) -> Option<Endianness> {
231        if self.read_bit(BIT_LITTLE_ENDIAN) {
232            Some(Endianness::Little)
233        } else if self.read_bit(BIT_BIG_ENDIAN) {
234            Some(Endianness::Big)
235        } else {
236            None
237        }
238    }
239
240    /// Set endianness of the memory access.
241    pub fn set_endianness(&mut self, endianness: Endianness) {
242        *self = self.with_endianness(endianness);
243    }
244
245    /// Set endianness of the memory access, returning new flags.
246    pub const fn with_endianness(self, endianness: Endianness) -> Self {
247        let res = match endianness {
248            Endianness::Little => self.with_bit(BIT_LITTLE_ENDIAN),
249            Endianness::Big => self.with_bit(BIT_BIG_ENDIAN),
250        };
251        assert!(!(res.read_bit(BIT_LITTLE_ENDIAN) && res.read_bit(BIT_BIG_ENDIAN)));
252        res
253    }
254
255    /// Test if this memory operation cannot trap.
256    ///
257    /// By default `MemFlags` will assume that any load/store can trap and is
258    /// associated with a `TrapCode::HeapOutOfBounds` code. If the trap code is
259    /// configured to `None` though then this method will return `true` and
260    /// indicates that the memory operation will not trap.
261    ///
262    /// If this returns `true` then the memory is *accessible*, which means
263    /// that accesses will not trap. This makes it possible to delete an unused
264    /// load or a dead store instruction.
265    ///
266    /// This flag does *not* mean that the associated instruction can be
267    /// code-motioned to arbitrary places in the function so long as its data
268    /// dependencies are met. This only means that, given its current location
269    /// in the function, it will never trap. See the `can_move` method for more
270    /// details.
271    pub const fn notrap(self) -> bool {
272        self.trap_code().is_none()
273    }
274
275    /// Sets the trap code for this `MemFlags` to `None`.
276    pub fn set_notrap(&mut self) {
277        *self = self.with_notrap();
278    }
279
280    /// Sets the trap code for this `MemFlags` to `None`, returning the new
281    /// flags.
282    pub const fn with_notrap(self) -> Self {
283        self.with_trap_code(None)
284    }
285
286    /// Is this memory operation safe to move so long as its data dependencies
287    /// remain satisfied?
288    ///
289    /// If this is `true`, then it is okay to code motion this instruction to
290    /// arbitrary locations, in the function, including across blocks and
291    /// conditional branches, so long as data dependencies (and trap ordering,
292    /// if any) are upheld.
293    ///
294    /// If this is `false`, then this memory operation's safety potentially
295    /// relies upon invariants that are not reflected in its data dependencies,
296    /// and therefore it is not safe to code motion this operation. For example,
297    /// this operation could be in a block that is dominated by a control-flow
298    /// bounds check, which is not reflected in its operands, and it would be
299    /// unsafe to code motion it above the bounds check, even if its data
300    /// dependencies would still be satisfied.
301    pub const fn can_move(self) -> bool {
302        self.read_bit(BIT_CAN_MOVE)
303    }
304
305    /// Set the `can_move` flag.
306    pub const fn set_can_move(&mut self) {
307        *self = self.with_can_move();
308    }
309
310    /// Set the `can_move` flag, returning new flags.
311    pub const fn with_can_move(self) -> Self {
312        self.with_bit(BIT_CAN_MOVE)
313    }
314
315    /// Test if the `aligned` flag is set.
316    ///
317    /// By default, Cranelift memory instructions work with any unaligned effective address. If the
318    /// `aligned` flag is set, the instruction is permitted to trap or return a wrong result if the
319    /// effective address is misaligned.
320    pub const fn aligned(self) -> bool {
321        self.read_bit(BIT_ALIGNED)
322    }
323
324    /// Set the `aligned` flag.
325    pub fn set_aligned(&mut self) {
326        *self = self.with_aligned();
327    }
328
329    /// Set the `aligned` flag, returning new flags.
330    pub const fn with_aligned(self) -> Self {
331        self.with_bit(BIT_ALIGNED)
332    }
333
334    /// Test if the `readonly` flag is set.
335    ///
336    /// Loads with this flag have no memory dependencies.
337    /// This results in undefined behavior if the dereferenced memory is mutated at any time
338    /// between when the function is called and when it is exited.
339    pub const fn readonly(self) -> bool {
340        self.read_bit(BIT_READONLY)
341    }
342
343    /// Set the `readonly` flag.
344    pub fn set_readonly(&mut self) {
345        *self = self.with_readonly();
346    }
347
348    /// Set the `readonly` flag, returning new flags.
349    pub const fn with_readonly(self) -> Self {
350        self.with_bit(BIT_READONLY)
351    }
352    /// Get the trap code to report if this memory access traps.
353    ///
354    /// A `None` trap code indicates that this memory access does not trap.
355    pub const fn trap_code(self) -> Option<TrapCode> {
356        let byte = ((self.bits & MASK_TRAP_CODE) >> TRAP_CODE_OFFSET) as u8;
357        match NonZeroU8::new(byte) {
358            Some(code) => Some(TrapCode::from_raw(code)),
359            None => None,
360        }
361    }
362
363    /// Configures these flags with the specified trap code `code`.
364    ///
365    /// A trap code indicates that this memory operation cannot be optimized
366    /// away and it must "stay where it is" in the programs. Traps are
367    /// considered side effects, for example, and have meaning through the trap
368    /// code that is communicated and which instruction trapped.
369    pub const fn with_trap_code(mut self, code: Option<TrapCode>) -> Self {
370        let bits = match code {
371            Some(code) => code.as_raw().get() as u16,
372            None => 0,
373        };
374        self.bits &= !MASK_TRAP_CODE;
375        self.bits |= bits << TRAP_CODE_OFFSET;
376        self
377    }
378}
379
380impl fmt::Display for MemFlags {
381    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
382        match self.trap_code() {
383            None => write!(f, " notrap")?,
384            // This is the default trap code, so don't print anything extra
385            // for this.
386            Some(TrapCode::HEAP_OUT_OF_BOUNDS) => {}
387            Some(t) => write!(f, " {t}")?,
388        }
389        if self.aligned() {
390            write!(f, " aligned")?;
391        }
392        if self.readonly() {
393            write!(f, " readonly")?;
394        }
395        if self.can_move() {
396            write!(f, " can_move")?;
397        }
398        if self.read_bit(BIT_BIG_ENDIAN) {
399            write!(f, " big")?;
400        }
401        if self.read_bit(BIT_LITTLE_ENDIAN) {
402            write!(f, " little")?;
403        }
404        match self.alias_region() {
405            None => {}
406            Some(AliasRegion::Heap) => write!(f, " heap")?,
407            Some(AliasRegion::Table) => write!(f, " table")?,
408            Some(AliasRegion::Vmctx) => write!(f, " vmctx")?,
409        }
410        Ok(())
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn roundtrip_traps() {
420        for trap in TrapCode::non_user_traps().iter().copied() {
421            let flags = MemFlags::new().with_trap_code(Some(trap));
422            assert_eq!(flags.trap_code(), Some(trap));
423        }
424        let flags = MemFlags::new().with_trap_code(None);
425        assert_eq!(flags.trap_code(), None);
426    }
427
428    #[test]
429    fn cannot_set_big_and_little() {
430        let mut big = MemFlags::new().with_endianness(Endianness::Big);
431        assert!(big.set_by_name("little").is_err());
432
433        let mut little = MemFlags::new().with_endianness(Endianness::Little);
434        assert!(little.set_by_name("big").is_err());
435    }
436
437    #[test]
438    fn only_one_region() {
439        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Heap));
440        assert!(big.set_by_name("table").is_err());
441        assert!(big.set_by_name("vmctx").is_err());
442
443        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Table));
444        assert!(big.set_by_name("heap").is_err());
445        assert!(big.set_by_name("vmctx").is_err());
446
447        let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Vmctx));
448        assert!(big.set_by_name("heap").is_err());
449        assert!(big.set_by_name("table").is_err());
450    }
451}