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 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}