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