rucc_opt/image.rs
1//! What a load from an object nothing can write to reads, which is what the object was
2//! initialized to.
3//!
4//! A `const` object with static storage duration and an initializer is bytes the program cannot
5//! change, so a load from one at an offset the compiler knows has an answer before the program
6//! runs. Working it out is the oldest optimization there is and rucc did not have it:
7//! `crate::load` forwards a load from a store earlier in the same block, and nothing anywhere
8//! looked at what a global was initialized to, so a `const` table read at a constant index kept
9//! its load and kept the arithmetic around it. That is issue 1358.
10//!
11//! # Where the image comes from
12//!
13//! A global's image lives on the module and a pass is handed one function, which is the problem
14//! `crate::extents` has and solves by running once over the module before the pipeline starts.
15//! That way out is wrong here, and finding out why is most of what this file is.
16//!
17//! What the frontend hands over for `t[2]` is not an offset. It is the index sign extended, a
18//! multiply by the element size, and a `ptr_add` of the product, because the lowering walk writes
19//! a subscript the way C defines one and leaves the arithmetic to the pipeline. So a step that ran
20//! before the pipeline would read an address it could not evaluate and fold nothing at all, on
21//! every array and every string in the program. `crate::extents` accepts exactly this cost for the
22//! same reason, and can, because what it loses is a bounds check it did not discharge. What is
23//! lost here is the whole optimization.
24//!
25//! So this is a pass, and the module's images reach it the way the machine does, on
26//! [`crate::Analyses`]. `crate::machine` argues that at length and the argument is the same one:
27//! the analysis cache is the one thing every pass is handed besides the function and its fuel, so
28//! a fact about the module that a pass needs goes there rather than onto a fourth parameter of
29//! every `run`. The pipeline builds one [`Images`] for the module and each function's cache holds
30//! a counted reference to it.
31//!
32//! The difference from the machine is that this is a table rather than two words, so the pipeline
33//! builds it only when a level runs this pass, and what it copies out of the module is the image
34//! of the globals that are read only and nothing else. A `const` table of a megabyte is copied
35//! once per compilation, which is the price of a pass being handed a function.
36//!
37//! # Where it runs
38//!
39//! With a `fold` on each side of it, at every level that optimizes and at none that does not.
40//!
41//! The one ahead is what turns the subscript arithmetic above into the offset this reads, so
42//! without it this answers nothing. The one behind is the mirror of that, and it is the half that
43//! is easy to leave out. What this writes is a constant where a load stood, and standing on top of
44//! it is whatever the program did with the value: `(int) one != 1` on a `const double` is a
45//! conversion and a comparison, and folding those is what turns the branch into a branch the
46//! control flow passes can take out. Nothing later in the list arrives in time, because the branch
47//! passes read the condition and a condition still spelled as a conversion of a constant is a
48//! branch they leave standing. That is the difference between a program that links and one that
49//! does not, which is what `gcc.c-torture/execute/20030216-1.c` is.
50//!
51//! # Which globals are believed
52//!
53//! The four conditions `crate::extents` sets, which is that module's `vouched`, and one more.
54//! The extra one is that writing through a pointer to the object is undefined, which is
55//! `Global::constant` and is what puts it in `.rodata`. A global that is not read only can be
56//! written by anything holding its address, including code in another translation unit, and this
57//! is not looking for the writes.
58//!
59//! Nothing here asks whether the object is reachable or whether its address is taken, because the
60//! question is what the bytes are rather than who else can see them. An exported `const` table
61//! folds for its own module and stays in the output for everybody else's.
62//!
63//! # What the image answers
64//!
65//! A run of zero bytes answers zero. A scalar answers the value it holds, when the access starts
66//! where the scalar does and is exactly as wide, because that is the case where no question of
67//! byte order arises: the image keeps a scalar as a value, and which byte of it comes first is
68//! decided when the object file is written rather than here. Literal bytes answer the number they
69//! spell in the order the datalayout puts them, which is where the byte order does have to be
70//! asked about and is the only place it is.
71//!
72//! The address of another symbol answers nothing, because a relocation has no value until the
73//! link. Neither does an access that crosses from one piece of the image into the next, since
74//! bytes spanning two of them are not a scalar either one holds, and neither does a `volatile`
75//! access or an atomic one, whose whole point is that the access happens.
76
77use std::collections::HashMap;
78use std::collections::hash_map::Entry;
79
80use rucc_base::Symbol;
81use rucc_ir::{
82 Block, Datum, Def, Extra, Flags, Func, Imm, Inst, MemOrder, Module, Opcode, Pic, Type, Value,
83};
84
85use crate::extents::vouched;
86use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
87
88/// Recorded once for each load that became a constant.
89const FOLDED: &str = "load from a read only object folded to what it was initialized to";
90
91/// Recorded for a load that would have folded if there had been fuel for it.
92const NO_FUEL: &str = "load from a read only object not folded, the pass ran out of fuel";
93
94/// What the pass is called, which [`crate::pipeline`] needs before it has run anything, to decide
95/// whether building the table is worth it.
96pub const NAME: &str = "image";
97
98/// The pass. It holds nothing, because the images are on the analysis cache.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct Image;
101
102impl Pass for Image {
103 fn name(&self) -> &'static str {
104 NAME
105 }
106
107 fn describe(&self) -> &'static str {
108 "a load from an object nothing can write to becomes what the object was initialized to"
109 }
110
111 fn preserves(&self) -> Preserved {
112 // A load becomes a constant where it stands, so no block moves and no edge moves. Not the
113 // liveness, for the reason `crate::fold` gives: the address the load was reading is read
114 // by nobody now.
115 Preserved::ALL.without(Analysis::Liveness)
116 }
117
118 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
119 let mut stats = Stats::new();
120 let images = an.images();
121 if images.is_empty() {
122 return stats;
123 }
124 let blocks: Vec<Block> = func.blocks().collect();
125 for block in blocks {
126 let insts: Vec<Inst> = func.insts(block).collect();
127 for inst in insts {
128 let Some((opcode, imm)) = answer(func, inst, images) else { continue };
129 if !fuel.take() {
130 // Out of fuel, which is a request to stop transforming rather than to stop
131 // looking, per `crate::fold`.
132 stats.missed(NO_FUEL);
133 continue;
134 }
135 let at = func.add_imm(imm);
136 let data = &mut func[inst];
137 data.opcode = opcode;
138 data.flags = Flags::NONE;
139 data.args = rucc_ir::ValueList::EMPTY;
140 data.extra = Extra::Imm(at);
141 stats.optimized(FOLDED);
142 }
143 }
144 stats
145 }
146}
147
148/// The initial image of every global in a module that a load can be answered out of.
149///
150/// Empty is the honest answer for a module with no such global and is also what a cache built
151/// without one holds, which is why there is a `Default` here and none on [`crate::Machine`]. A
152/// missing cost table is a pass optimizing for a machine nobody chose; a missing image is a load
153/// that does not fold.
154#[derive(Debug, Default, Clone)]
155pub struct Images {
156 /// By the name the global is reached by, and `None` for a name that arrived twice.
157 objects: HashMap<Symbol, Option<Object>>,
158 /// Which end of a number the target puts first, which only the literal bytes need.
159 little_endian: bool,
160}
161
162impl Images {
163 /// The images of every read only global this module can vouch for.
164 #[must_use]
165 pub fn of(module: &Module, pic: Pic) -> Self {
166 let mut objects: HashMap<Symbol, Option<Object>> = HashMap::new();
167 for id in module.globals() {
168 let global = &module[id];
169 if !global.constant || !vouched(global, pic) {
170 continue;
171 }
172 let object = Object::of(module, global);
173 // A name that somehow arrives twice keeps neither image. That cannot happen in a
174 // module the frontend built, and written this way the failure if it ever does is a
175 // load that did not fold rather than a load folded out of the wrong object.
176 match objects.entry(global.name) {
177 Entry::Occupied(mut at) => *at.get_mut() = None,
178 Entry::Vacant(at) => {
179 at.insert(Some(object));
180 }
181 }
182 }
183 Self { objects, little_endian: module.datalayout.little_endian }
184 }
185
186 /// Whether that name is read only data this module defines and nothing else can replace.
187 ///
188 /// Which is also a name whose distance from anything else in this file is a number once the
189 /// program is linked, and that is what `crate::switch_conv` asks this for.
190 #[must_use]
191 pub fn holds(&self, name: Symbol) -> bool {
192 self.objects.contains_key(&name)
193 }
194
195 /// Whether there is anything here to answer a load with.
196 #[must_use]
197 pub fn is_empty(&self) -> bool {
198 self.objects.is_empty()
199 }
200
201 /// The value of type `ty` that lies `offset` bytes into the image of that global.
202 #[must_use]
203 pub fn read(&self, name: Symbol, ty: Type, offset: u64) -> Option<Imm> {
204 let object = self.objects.get(&name)?.as_ref()?;
205 let size = u64::from(ty.bits().div_ceil(8));
206 let end = offset.checked_add(size)?;
207 if size == 0 || end > object.size {
208 return None;
209 }
210 let mut at = 0u64;
211 for piece in &object.pieces {
212 let width = piece.size();
213 if at + width > offset {
214 // The piece the access starts in, and the only one it may read, so an access
215 // reaching past the end of this one is an access this cannot answer.
216 return (at + width >= end)
217 .then(|| piece.read(ty, offset - at, size, self.little_endian))?;
218 }
219 at += width;
220 }
221 None
222 }
223}
224
225/// One global's image, in the pieces the module wrote it in.
226#[derive(Debug, Clone)]
227struct Object {
228 /// How many bytes the object is, which the pieces add up to.
229 size: u64,
230 /// What is in them, in order.
231 pieces: Vec<Piece>,
232}
233
234impl Object {
235 /// The image of a global this module defines.
236 fn of(module: &Module, global: &rucc_ir::Global) -> Self {
237 let data = global.init.map(|list| &module[list]).unwrap_or_default();
238 let pieces = data
239 .iter()
240 .map(|&datum| match datum {
241 Datum::Zero(bytes) => Piece::Zero(bytes),
242 Datum::Bytes(range) => Piece::Bytes(module[range].to_vec()),
243 Datum::Scalar { ty, value } => Piece::Scalar { ty, value: module[value] },
244 // Kept rather than dropped, so that what follows it is still at the offset it is
245 // at. What it holds is an address the linker has not written yet.
246 Datum::Addr(_) | Datum::Away(_) | Datum::Apart { .. } => {
247 Piece::Opaque(datum.size(module))
248 }
249 })
250 .collect();
251 Self { size: global.size, pieces }
252 }
253}
254
255/// One piece of an image, which is a [`Datum`] with what it refers to copied out of the module.
256#[derive(Debug, Clone)]
257enum Piece {
258 /// That many zero bytes.
259 Zero(u64),
260 /// Those literal bytes.
261 Bytes(Vec<u8>),
262 /// One scalar of that type holding that value.
263 Scalar { ty: Type, value: Imm },
264 /// That many bytes whose value this cannot say.
265 Opaque(u64),
266}
267
268impl Piece {
269 /// How many bytes of the image it is.
270 fn size(&self) -> u64 {
271 match self {
272 Self::Zero(bytes) | Self::Opaque(bytes) => *bytes,
273 Self::Bytes(bytes) => bytes.len() as u64,
274 Self::Scalar { ty, .. } => u64::from(ty.bits().div_ceil(8)) * u64::from(ty.lanes()),
275 }
276 }
277
278 /// The value an access of `size` bytes `into` this piece reads, as a constant of type `ty`.
279 fn read(&self, ty: Type, into: u64, size: u64, little_endian: bool) -> Option<Imm> {
280 match self {
281 Self::Zero(_) => Some(number(ty, 0)),
282 // Exactly this scalar and no part of it, which is the case byte order has no say in.
283 // The image holds a scalar as a value rather than as bytes, so what comes back is what
284 // was written, and an access of the same width starting where it starts reads it
285 // whichever end of it the target puts first.
286 Self::Scalar { ty: held, value } => {
287 (into == 0 && held.is_scalar() && u64::from(held.bits().div_ceil(8)) == size)
288 .then_some(*value)
289 }
290 Self::Bytes(bytes) => {
291 let into = usize::try_from(into).ok()?;
292 let size = usize::try_from(size).ok()?;
293 let bytes = bytes.get(into..into.checked_add(size)?)?;
294 Some(number(ty, assemble(bytes, little_endian)))
295 }
296 // A relocation is a promise the linker has not kept yet, so there is no number here to
297 // read at all. This is where `&other` written into an initializer stops.
298 Self::Opaque(_) => None,
299 }
300 }
301}
302
303/// What this instruction reads, if it is a load the images can answer.
304///
305/// The opcode comes back with the value because a constant of an integer type and a constant of a
306/// floating point type are two different instructions, and which one to write is decided by the
307/// type of the load rather than by what the image turned out to hold.
308fn answer(func: &Func, inst: Inst, images: &Images) -> Option<(Opcode, Imm)> {
309 let data = &func[inst];
310 if data.opcode != Opcode::Load || data.results != 1 || data.flags.contains(Flags::VOLATILE) {
311 return None;
312 }
313 let Extra::Mem(info) = data.extra else { return None };
314 if func[info].order != MemOrder::NotAtomic {
315 return None;
316 }
317 let ty = func[data.results().next()?].ty;
318 // A vector constant is a `splat` rather than an `iconst`, which is the reason `crate::fold`
319 // gives for leaving one alone, and there is a second reason on top of it here: the image would
320 // have to be read a lane at a time and every lane would have to agree.
321 if !ty.is_scalar() || !(ty.is_int() || ty.is_float()) {
322 return None;
323 }
324 let (base, offset) = address(func, *func[data.args].first()?)?;
325 let Def::Result { inst: made, .. } = func[base].def else { return None };
326 if func[made].opcode != Opcode::GlobalAddr {
327 return None;
328 }
329 let Extra::Symbol(name) = func[made].extra else { return None };
330 let imm = images.read(name, ty, u64::try_from(offset).ok()?)?;
331 Some((if ty.is_int() { Opcode::IConst } else { Opcode::FConst }, imm))
332}
333
334/// The address this value is, as something it was computed from and a distance in bytes from it.
335///
336/// A `ptr_add` of a constant, as many times over as there are of them, because an index into an
337/// array of structures is one of these per level and the frontend writes them one at a time.
338/// Anything else ends the walk and is what comes back, which for a load from a global is the
339/// `global_addr` and for every other load is something the caller will not recognise.
340fn address(func: &Func, mut value: Value) -> Option<(Value, i128)> {
341 let mut offset: i128 = 0;
342 loop {
343 let Def::Result { inst, .. } = func[value].def else { return Some((value, offset)) };
344 if func[inst].opcode != Opcode::PtrAdd {
345 return Some((value, offset));
346 }
347 let args = &func[func[inst].args];
348 let (step, step_ty) = crate::fold::constant(func, *args.get(1)?)?;
349 offset = offset.checked_add(step.signed(step_ty))?;
350 value = *args.first()?;
351 }
352}
353
354/// Those bytes as one number, in the order the target reads them in.
355fn assemble(bytes: &[u8], little_endian: bool) -> u128 {
356 let mut value = 0u128;
357 // Most significant byte first, which is the last of them on a little endian target and the
358 // first of them on a big endian one.
359 if little_endian {
360 for &byte in bytes.iter().rev() {
361 value = value << 8 | u128::from(byte);
362 }
363 } else {
364 for &byte in bytes {
365 value = value << 8 | u128::from(byte);
366 }
367 }
368 value
369}
370
371/// Those bits as a constant of that type, which is a value for an integer and a bit pattern for a
372/// floating point number.
373fn number(ty: Type, bits: u128) -> Imm {
374 if ty.is_int() { Imm::int(bits as i128, ty) } else { Imm::from_bits(bits) }
375}
376
377#[cfg(test)]
378mod tests {
379 use rucc_base::Interner;
380 use rucc_ir::{Datum, Global, Imm, Linkage, Module, Pic, Reloc, Type};
381 use rucc_target::{TargetInfo, Triple};
382
383 use super::Images;
384
385 /// The images of a module with one read only global named `g`, holding that.
386 ///
387 /// The size comes from the data rather than from the caller, so that a test saying what is in
388 /// the object does not also have to say how long it is and cannot say the two differently.
389 fn images(build: impl Fn(&mut Module) -> Vec<Datum>) -> (Interner, Images) {
390 let mut names = Interner::new();
391 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
392 let mut module = Module::new(names.intern("t.c"), &target);
393 let data = build(&mut module);
394 let size = data.iter().map(|datum| datum.size(&module)).sum();
395 let mut global = Global::new(names.intern("g"), size, 8);
396 global.linkage = Linkage::Internal;
397 global.constant = true;
398 global.init = Some(module.push_data(&data));
399 module.add_global(global);
400 let images = Images::of(&module, Pic::Executable);
401 (names, images)
402 }
403
404 /// What a load of that type from that offset into `g` reads.
405 fn read(names: &mut Interner, images: &Images, ty: Type, offset: u64) -> Option<Imm> {
406 images.read(names.intern("g"), ty, offset)
407 }
408
409 /// The four bytes of `10, 20, 30, 40` as an `int` array is written, which is the case the
410 /// whole pass exists for.
411 #[test]
412 fn a_slot_of_a_table_reads_what_the_table_was_initialized_to() {
413 let (mut names, images) = images(|module| {
414 [10i128, 20, 30, 40]
415 .into_iter()
416 .map(|value| Datum::Scalar {
417 ty: Type::int(32),
418 value: module.add_imm(Imm::int(value, Type::int(32))),
419 })
420 .collect()
421 });
422 let mut at = |offset| read(&mut names, &images, Type::int(32), offset).map(Imm::unsigned);
423 assert_eq!(at(0), Some(10));
424 assert_eq!(at(8), Some(30));
425 assert_eq!(at(12), Some(40));
426 }
427
428 /// One byte of a string literal, which arrives as literal bytes rather than as scalars.
429 #[test]
430 fn a_byte_of_a_string_reads_the_byte_the_string_spells() {
431 let (mut names, images) = images(|module| vec![Datum::Bytes(module.push_bytes(b"abc\0"))]);
432 let mut at = |offset| read(&mut names, &images, Type::int(8), offset).map(Imm::unsigned);
433 assert_eq!(at(0), Some(u128::from(b'a')));
434 assert_eq!(at(1), Some(u128::from(b'b')));
435 assert_eq!(at(3), Some(0));
436 assert_eq!(at(4), None, "one past the end of the object");
437 }
438
439 /// Several bytes at once, which is the one place the target's byte order has anything to say.
440 #[test]
441 fn several_bytes_read_as_a_number_in_the_order_the_target_puts_them() {
442 let (mut names, images) =
443 images(|module| vec![Datum::Bytes(module.push_bytes(&[1, 2, 3, 4]))]);
444 assert_eq!(
445 read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
446 Some(0x0403_0201),
447 "least significant byte first, which is what x86-64 is"
448 );
449 }
450
451 /// A run of zeroes, which is how the tail of a partly initialized object is written.
452 #[test]
453 fn a_run_of_zeroes_reads_zero() {
454 let (mut names, images) = images(|_| vec![Datum::Zero(16)]);
455 assert_eq!(read(&mut names, &images, Type::int(64), 8).map(Imm::unsigned), Some(0));
456 }
457
458 /// Half of a scalar, which the image cannot answer because it does not hold the scalar as
459 /// bytes and the question is about bytes.
460 #[test]
461 fn a_part_of_a_scalar_is_not_read() {
462 let (mut names, images) = images(|module| {
463 vec![Datum::Scalar {
464 ty: Type::int(32),
465 value: module.add_imm(Imm::int(0x0403_0201, Type::int(32))),
466 }]
467 });
468 assert_eq!(read(&mut names, &images, Type::int(8), 0), None);
469 assert_eq!(read(&mut names, &images, Type::int(16), 2), None);
470 assert_eq!(
471 read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
472 Some(0x0403_0201)
473 );
474 }
475
476 /// An access starting in one piece and ending in the next, which is a number neither of them
477 /// holds.
478 #[test]
479 fn an_access_that_crosses_from_one_piece_into_the_next_is_not_read() {
480 let (mut names, images) = images(|module| {
481 vec![Datum::Bytes(module.push_bytes(&[1, 2])), Datum::Bytes(module.push_bytes(&[3, 4]))]
482 });
483 assert_eq!(read(&mut names, &images, Type::int(32), 0), None);
484 assert_eq!(read(&mut names, &images, Type::int(16), 0).map(Imm::unsigned), Some(0x0201));
485 assert_eq!(read(&mut names, &images, Type::int(16), 2).map(Imm::unsigned), Some(0x0403));
486 }
487
488 /// The address of another symbol, which has no value until the link.
489 #[test]
490 fn the_address_of_something_else_is_not_read() {
491 let (mut names, images) = images(|module| {
492 let symbol = module.name;
493 let to = module.add_reloc(Reloc { symbol, addend: 0, size: 8 });
494 vec![Datum::Addr(to), Datum::Bytes(module.push_bytes(&[7]))]
495 });
496 assert_eq!(read(&mut names, &images, Type::PTR, 0), None);
497 assert_eq!(
498 read(&mut names, &images, Type::int(8), 8).map(Imm::unsigned),
499 Some(7),
500 "what follows a relocation is still where it was"
501 );
502 }
503
504 /// Past the end of the object, which is a program that has already gone wrong and is not a
505 /// program this answers.
506 #[test]
507 fn past_the_end_of_the_object_is_not_read() {
508 let (mut names, images) = images(|_| vec![Datum::Zero(4)]);
509 assert_eq!(read(&mut names, &images, Type::int(32), 4), None);
510 assert_eq!(read(&mut names, &images, Type::int(64), 0), None);
511 }
512
513 /// A global something can write to, which is every global this does not look at.
514 #[test]
515 fn a_global_that_is_not_read_only_has_no_image() {
516 let mut names = Interner::new();
517 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
518 let mut module = Module::new(names.intern("t.c"), &target);
519 let mut global = Global::new(names.intern("g"), 4, 4);
520 global.linkage = Linkage::Internal;
521 global.init = Some(module.push_data(&[Datum::Zero(4)]));
522 module.add_global(global);
523 let images = Images::of(&module, Pic::Executable);
524 assert!(images.is_empty());
525 assert_eq!(images.read(names.intern("g"), Type::int(32), 0), None);
526 }
527}