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 there is anything here to answer a load with.
187 #[must_use]
188 pub fn is_empty(&self) -> bool {
189 self.objects.is_empty()
190 }
191
192 /// The value of type `ty` that lies `offset` bytes into the image of that global.
193 #[must_use]
194 pub fn read(&self, name: Symbol, ty: Type, offset: u64) -> Option<Imm> {
195 let object = self.objects.get(&name)?.as_ref()?;
196 let size = u64::from(ty.bits().div_ceil(8));
197 let end = offset.checked_add(size)?;
198 if size == 0 || end > object.size {
199 return None;
200 }
201 let mut at = 0u64;
202 for piece in &object.pieces {
203 let width = piece.size();
204 if at + width > offset {
205 // The piece the access starts in, and the only one it may read, so an access
206 // reaching past the end of this one is an access this cannot answer.
207 return (at + width >= end)
208 .then(|| piece.read(ty, offset - at, size, self.little_endian))?;
209 }
210 at += width;
211 }
212 None
213 }
214}
215
216/// One global's image, in the pieces the module wrote it in.
217#[derive(Debug, Clone)]
218struct Object {
219 /// How many bytes the object is, which the pieces add up to.
220 size: u64,
221 /// What is in them, in order.
222 pieces: Vec<Piece>,
223}
224
225impl Object {
226 /// The image of a global this module defines.
227 fn of(module: &Module, global: &rucc_ir::Global) -> Self {
228 let data = global.init.map(|list| &module[list]).unwrap_or_default();
229 let pieces = data
230 .iter()
231 .map(|&datum| match datum {
232 Datum::Zero(bytes) => Piece::Zero(bytes),
233 Datum::Bytes(range) => Piece::Bytes(module[range].to_vec()),
234 Datum::Scalar { ty, value } => Piece::Scalar { ty, value: module[value] },
235 // Kept rather than dropped, so that what follows it is still at the offset it is
236 // at. What it holds is an address the linker has not written yet.
237 Datum::Addr(_) | Datum::Away(_) | Datum::Apart { .. } => {
238 Piece::Opaque(datum.size(module))
239 }
240 })
241 .collect();
242 Self { size: global.size, pieces }
243 }
244}
245
246/// One piece of an image, which is a [`Datum`] with what it refers to copied out of the module.
247#[derive(Debug, Clone)]
248enum Piece {
249 /// That many zero bytes.
250 Zero(u64),
251 /// Those literal bytes.
252 Bytes(Vec<u8>),
253 /// One scalar of that type holding that value.
254 Scalar { ty: Type, value: Imm },
255 /// That many bytes whose value this cannot say.
256 Opaque(u64),
257}
258
259impl Piece {
260 /// How many bytes of the image it is.
261 fn size(&self) -> u64 {
262 match self {
263 Self::Zero(bytes) | Self::Opaque(bytes) => *bytes,
264 Self::Bytes(bytes) => bytes.len() as u64,
265 Self::Scalar { ty, .. } => u64::from(ty.bits().div_ceil(8)) * u64::from(ty.lanes()),
266 }
267 }
268
269 /// The value an access of `size` bytes `into` this piece reads, as a constant of type `ty`.
270 fn read(&self, ty: Type, into: u64, size: u64, little_endian: bool) -> Option<Imm> {
271 match self {
272 Self::Zero(_) => Some(number(ty, 0)),
273 // Exactly this scalar and no part of it, which is the case byte order has no say in.
274 // The image holds a scalar as a value rather than as bytes, so what comes back is what
275 // was written, and an access of the same width starting where it starts reads it
276 // whichever end of it the target puts first.
277 Self::Scalar { ty: held, value } => {
278 (into == 0 && held.is_scalar() && u64::from(held.bits().div_ceil(8)) == size)
279 .then_some(*value)
280 }
281 Self::Bytes(bytes) => {
282 let into = usize::try_from(into).ok()?;
283 let size = usize::try_from(size).ok()?;
284 let bytes = bytes.get(into..into.checked_add(size)?)?;
285 Some(number(ty, assemble(bytes, little_endian)))
286 }
287 // A relocation is a promise the linker has not kept yet, so there is no number here to
288 // read at all. This is where `&other` written into an initializer stops.
289 Self::Opaque(_) => None,
290 }
291 }
292}
293
294/// What this instruction reads, if it is a load the images can answer.
295///
296/// The opcode comes back with the value because a constant of an integer type and a constant of a
297/// floating point type are two different instructions, and which one to write is decided by the
298/// type of the load rather than by what the image turned out to hold.
299fn answer(func: &Func, inst: Inst, images: &Images) -> Option<(Opcode, Imm)> {
300 let data = &func[inst];
301 if data.opcode != Opcode::Load || data.results != 1 || data.flags.contains(Flags::VOLATILE) {
302 return None;
303 }
304 let Extra::Mem(info) = data.extra else { return None };
305 if func[info].order != MemOrder::NotAtomic {
306 return None;
307 }
308 let ty = func[data.results().next()?].ty;
309 // A vector constant is a `splat` rather than an `iconst`, which is the reason `crate::fold`
310 // gives for leaving one alone, and there is a second reason on top of it here: the image would
311 // have to be read a lane at a time and every lane would have to agree.
312 if !ty.is_scalar() || !(ty.is_int() || ty.is_float()) {
313 return None;
314 }
315 let (base, offset) = address(func, *func[data.args].first()?)?;
316 let Def::Result { inst: made, .. } = func[base].def else { return None };
317 if func[made].opcode != Opcode::GlobalAddr {
318 return None;
319 }
320 let Extra::Symbol(name) = func[made].extra else { return None };
321 let imm = images.read(name, ty, u64::try_from(offset).ok()?)?;
322 Some((if ty.is_int() { Opcode::IConst } else { Opcode::FConst }, imm))
323}
324
325/// The address this value is, as something it was computed from and a distance in bytes from it.
326///
327/// A `ptr_add` of a constant, as many times over as there are of them, because an index into an
328/// array of structures is one of these per level and the frontend writes them one at a time.
329/// Anything else ends the walk and is what comes back, which for a load from a global is the
330/// `global_addr` and for every other load is something the caller will not recognise.
331fn address(func: &Func, mut value: Value) -> Option<(Value, i128)> {
332 let mut offset: i128 = 0;
333 loop {
334 let Def::Result { inst, .. } = func[value].def else { return Some((value, offset)) };
335 if func[inst].opcode != Opcode::PtrAdd {
336 return Some((value, offset));
337 }
338 let args = &func[func[inst].args];
339 let (step, step_ty) = crate::fold::constant(func, *args.get(1)?)?;
340 offset = offset.checked_add(step.signed(step_ty))?;
341 value = *args.first()?;
342 }
343}
344
345/// Those bytes as one number, in the order the target reads them in.
346fn assemble(bytes: &[u8], little_endian: bool) -> u128 {
347 let mut value = 0u128;
348 // Most significant byte first, which is the last of them on a little endian target and the
349 // first of them on a big endian one.
350 if little_endian {
351 for &byte in bytes.iter().rev() {
352 value = value << 8 | u128::from(byte);
353 }
354 } else {
355 for &byte in bytes {
356 value = value << 8 | u128::from(byte);
357 }
358 }
359 value
360}
361
362/// Those bits as a constant of that type, which is a value for an integer and a bit pattern for a
363/// floating point number.
364fn number(ty: Type, bits: u128) -> Imm {
365 if ty.is_int() { Imm::int(bits as i128, ty) } else { Imm::from_bits(bits) }
366}
367
368#[cfg(test)]
369mod tests {
370 use rucc_base::Interner;
371 use rucc_ir::{Datum, Global, Imm, Linkage, Module, Pic, Reloc, Type};
372 use rucc_target::{TargetInfo, Triple};
373
374 use super::Images;
375
376 /// The images of a module with one read only global named `g`, holding that.
377 ///
378 /// The size comes from the data rather than from the caller, so that a test saying what is in
379 /// the object does not also have to say how long it is and cannot say the two differently.
380 fn images(build: impl Fn(&mut Module) -> Vec<Datum>) -> (Interner, Images) {
381 let mut names = Interner::new();
382 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
383 let mut module = Module::new(names.intern("t.c"), &target);
384 let data = build(&mut module);
385 let size = data.iter().map(|datum| datum.size(&module)).sum();
386 let mut global = Global::new(names.intern("g"), size, 8);
387 global.linkage = Linkage::Internal;
388 global.constant = true;
389 global.init = Some(module.push_data(&data));
390 module.add_global(global);
391 let images = Images::of(&module, Pic::Executable);
392 (names, images)
393 }
394
395 /// What a load of that type from that offset into `g` reads.
396 fn read(names: &mut Interner, images: &Images, ty: Type, offset: u64) -> Option<Imm> {
397 images.read(names.intern("g"), ty, offset)
398 }
399
400 /// The four bytes of `10, 20, 30, 40` as an `int` array is written, which is the case the
401 /// whole pass exists for.
402 #[test]
403 fn a_slot_of_a_table_reads_what_the_table_was_initialized_to() {
404 let (mut names, images) = images(|module| {
405 [10i128, 20, 30, 40]
406 .into_iter()
407 .map(|value| Datum::Scalar {
408 ty: Type::int(32),
409 value: module.add_imm(Imm::int(value, Type::int(32))),
410 })
411 .collect()
412 });
413 let mut at = |offset| read(&mut names, &images, Type::int(32), offset).map(Imm::unsigned);
414 assert_eq!(at(0), Some(10));
415 assert_eq!(at(8), Some(30));
416 assert_eq!(at(12), Some(40));
417 }
418
419 /// One byte of a string literal, which arrives as literal bytes rather than as scalars.
420 #[test]
421 fn a_byte_of_a_string_reads_the_byte_the_string_spells() {
422 let (mut names, images) = images(|module| vec![Datum::Bytes(module.push_bytes(b"abc\0"))]);
423 let mut at = |offset| read(&mut names, &images, Type::int(8), offset).map(Imm::unsigned);
424 assert_eq!(at(0), Some(u128::from(b'a')));
425 assert_eq!(at(1), Some(u128::from(b'b')));
426 assert_eq!(at(3), Some(0));
427 assert_eq!(at(4), None, "one past the end of the object");
428 }
429
430 /// Several bytes at once, which is the one place the target's byte order has anything to say.
431 #[test]
432 fn several_bytes_read_as_a_number_in_the_order_the_target_puts_them() {
433 let (mut names, images) =
434 images(|module| vec![Datum::Bytes(module.push_bytes(&[1, 2, 3, 4]))]);
435 assert_eq!(
436 read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
437 Some(0x0403_0201),
438 "least significant byte first, which is what x86-64 is"
439 );
440 }
441
442 /// A run of zeroes, which is how the tail of a partly initialized object is written.
443 #[test]
444 fn a_run_of_zeroes_reads_zero() {
445 let (mut names, images) = images(|_| vec![Datum::Zero(16)]);
446 assert_eq!(read(&mut names, &images, Type::int(64), 8).map(Imm::unsigned), Some(0));
447 }
448
449 /// Half of a scalar, which the image cannot answer because it does not hold the scalar as
450 /// bytes and the question is about bytes.
451 #[test]
452 fn a_part_of_a_scalar_is_not_read() {
453 let (mut names, images) = images(|module| {
454 vec![Datum::Scalar {
455 ty: Type::int(32),
456 value: module.add_imm(Imm::int(0x0403_0201, Type::int(32))),
457 }]
458 });
459 assert_eq!(read(&mut names, &images, Type::int(8), 0), None);
460 assert_eq!(read(&mut names, &images, Type::int(16), 2), None);
461 assert_eq!(
462 read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
463 Some(0x0403_0201)
464 );
465 }
466
467 /// An access starting in one piece and ending in the next, which is a number neither of them
468 /// holds.
469 #[test]
470 fn an_access_that_crosses_from_one_piece_into_the_next_is_not_read() {
471 let (mut names, images) = images(|module| {
472 vec![Datum::Bytes(module.push_bytes(&[1, 2])), Datum::Bytes(module.push_bytes(&[3, 4]))]
473 });
474 assert_eq!(read(&mut names, &images, Type::int(32), 0), None);
475 assert_eq!(read(&mut names, &images, Type::int(16), 0).map(Imm::unsigned), Some(0x0201));
476 assert_eq!(read(&mut names, &images, Type::int(16), 2).map(Imm::unsigned), Some(0x0403));
477 }
478
479 /// The address of another symbol, which has no value until the link.
480 #[test]
481 fn the_address_of_something_else_is_not_read() {
482 let (mut names, images) = images(|module| {
483 let symbol = module.name;
484 let to = module.add_reloc(Reloc { symbol, addend: 0, size: 8 });
485 vec![Datum::Addr(to), Datum::Bytes(module.push_bytes(&[7]))]
486 });
487 assert_eq!(read(&mut names, &images, Type::PTR, 0), None);
488 assert_eq!(
489 read(&mut names, &images, Type::int(8), 8).map(Imm::unsigned),
490 Some(7),
491 "what follows a relocation is still where it was"
492 );
493 }
494
495 /// Past the end of the object, which is a program that has already gone wrong and is not a
496 /// program this answers.
497 #[test]
498 fn past_the_end_of_the_object_is_not_read() {
499 let (mut names, images) = images(|_| vec![Datum::Zero(4)]);
500 assert_eq!(read(&mut names, &images, Type::int(32), 4), None);
501 assert_eq!(read(&mut names, &images, Type::int(64), 0), None);
502 }
503
504 /// A global something can write to, which is every global this does not look at.
505 #[test]
506 fn a_global_that_is_not_read_only_has_no_image() {
507 let mut names = Interner::new();
508 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
509 let mut module = Module::new(names.intern("t.c"), &target);
510 let mut global = Global::new(names.intern("g"), 4, 4);
511 global.linkage = Linkage::Internal;
512 global.init = Some(module.push_data(&[Datum::Zero(4)]));
513 module.add_global(global);
514 let images = Images::of(&module, Pic::Executable);
515 assert!(images.is_empty());
516 assert_eq!(images.read(names.intern("g"), Type::int(32), 0), None);
517 }
518}