1 //! Memory operation flags.
2 
3 use super::TrapCode;
4 use core::fmt;
5 use core::num::NonZeroU8;
6 use core::str::FromStr;
7 
8 #[cfg(feature = "enable-serde")]
9 use serde_derive::{Deserialize, Serialize};
10 
11 /// Endianness of a memory access.
12 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
13 pub 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]
26 pub enum AliasRegion {
27     // None = 0b00;
28     Heap    = 0b01,
29     Table   = 0b10,
30     Vmctx   = 0b11,
31 }
32 
33 impl AliasRegion {
from_bits(bits: u8) -> Option<Self>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 
to_bits(region: Option<Self>) -> u844     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))]
64 pub 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.
87 const 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.
92 const BIT_READONLY: u16 = 1 << 1;
93 
94 /// Load multi-byte values from memory in a little-endian format.
95 const BIT_LITTLE_ENDIAN: u16 = 1 << 2;
96 
97 /// Load multi-byte values from memory in a big-endian format.
98 const BIT_BIG_ENDIAN: u16 = 1 << 3;
99 
100 /// Used for alias analysis, indicates which disjoint part of the abstract state
101 /// is being accessed.
102 const MASK_ALIAS_REGION: u16 = 0b11 << ALIAS_REGION_OFFSET;
103 const ALIAS_REGION_OFFSET: u16 = 5;
104 
105 /// Trap code, if any, for this memory operation.
106 const MASK_TRAP_CODE: u16 = 0b1111_1111 << TRAP_CODE_OFFSET;
107 const 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.
114 const BIT_CAN_MOVE: u16 = 1 << 15;
115 
116 impl MemFlags {
117     /// Create a new empty set of flags.
new() -> Self118     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.
trusted() -> Self124     pub const fn trusted() -> Self {
125         Self::new().with_notrap().with_aligned()
126     }
127 
128     /// Read a flag bit.
read_bit(self, bit: u16) -> bool129     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.
with_bit(mut self, bit: u16) -> Self134     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.
alias_region(self) -> Option<AliasRegion>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`.
with_alias_region(mut self, region: Option<AliasRegion>) -> Self145     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`.
set_alias_region(&mut self, region: Option<AliasRegion>)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.
set_by_name(&mut self, name: &str) -> Result<bool, &'static str>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.
endianness(self, native_endianness: Endianness) -> Endianness216     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".
explicit_endianness(self) -> Option<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.
set_endianness(&mut self, endianness: Endianness)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.
with_endianness(self, endianness: Endianness) -> Self246     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.
notrap(self) -> bool271     pub const fn notrap(self) -> bool {
272         self.trap_code().is_none()
273     }
274 
275     /// Sets the trap code for this `MemFlags` to `None`.
set_notrap(&mut self)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.
with_notrap(self) -> Self282     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.
can_move(self) -> bool301     pub const fn can_move(self) -> bool {
302         self.read_bit(BIT_CAN_MOVE)
303     }
304 
305     /// Set the `can_move` flag.
set_can_move(&mut self)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.
with_can_move(self) -> Self311     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.
aligned(self) -> bool320     pub const fn aligned(self) -> bool {
321         self.read_bit(BIT_ALIGNED)
322     }
323 
324     /// Set the `aligned` flag.
set_aligned(&mut self)325     pub fn set_aligned(&mut self) {
326         *self = self.with_aligned();
327     }
328 
329     /// Set the `aligned` flag, returning new flags.
with_aligned(self) -> Self330     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.
readonly(self) -> bool339     pub const fn readonly(self) -> bool {
340         self.read_bit(BIT_READONLY)
341     }
342 
343     /// Set the `readonly` flag.
set_readonly(&mut self)344     pub fn set_readonly(&mut self) {
345         *self = self.with_readonly();
346     }
347 
348     /// Set the `readonly` flag, returning new flags.
with_readonly(self) -> Self349     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.
trap_code(self) -> Option<TrapCode>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.
with_trap_code(mut self, code: Option<TrapCode>) -> Self369     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 
380 impl fmt::Display for MemFlags {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result381     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)]
415 mod tests {
416     use super::*;
417 
418     #[test]
roundtrip_traps()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]
cannot_set_big_and_little()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]
only_one_region()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 }
452