Skip to main content

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}