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