灵符lingfu

ling-mir

↓ 0 downloads

Mid-level IR with real optimization passes

Keywords mir, compiler, optimizer, ir
lingfu install ling-mir
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (29)

src/optimizations/licm.rs
use crate::ir::*;
use crate::loop_utils;
use crate::optimizations::Transform;
use ling_ast::Span;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};

pub struct Licm;

impl Transform for Licm {
    fn run(&self, func: &mut MirFunction) -> bool {
        let mut changed = false;
        let loops = loop_utils::find_loops(func);
        let mut processed_headers = HashSet::default();

        for lp in loops {
            if lp.header.0 == 0 || processed_headers.contains(&lp.header) {
                continue;
            }
            processed_headers.insert(lp.header);

            if self.optimize_loop(func, &lp) {
                changed = true;
                break;
            }
        }

        changed
    }
}

impl Licm {
    fn optimize_loop(&self, func: &mut MirFunction, lp: &loop_utils::Loop) -> bool {
        let mut assign_counts: HashMap<Local, usize> = HashMap::default();
        for bb in &func.basic_blocks {
            for stmt in &bb.statements {
                if let StatementKind::Assign(local, _) = &stmt.kind {
                    *assign_counts.entry(*local).or_insert(0) += 1;
                } else if let StatementKind::SetAttr(Operand::Copy(l) | Operand::Move(l), _, _) =
                    &stmt.kind
                {
                    *assign_counts.entry(*l).or_insert(0) += 1;
                } else if let StatementKind::SetIndex(Operand::Copy(l) | Operand::Move(l), _, _) =
                    &stmt.kind
                {
                    *assign_counts.entry(*l).or_insert(0) += 1;
                } else if let StatementKind::VectorStore(
                    Operand::Copy(l) | Operand::Move(l),
                    _,
                    _,
                ) = &stmt.kind
                {
                    *assign_counts.entry(*l).or_insert(0) += 1;
                }
            }
        }

        let mut defined_in_loop = HashSet::default();
        for &bb_id in &lp.body {
            let bb = &func.basic_blocks[bb_id.0];
            for stmt in &bb.statements {
                if let StatementKind::Assign(local, _) = &stmt.kind {
                    defined_in_loop.insert(*local);
                } else if let StatementKind::SetAttr(Operand::Copy(l) | Operand::Move(l), _, _) =
                    &stmt.kind
                {
                    defined_in_loop.insert(*l);
                } else if let StatementKind::SetIndex(Operand::Copy(l) | Operand::Move(l), _, _) =
                    &stmt.kind
                {
                    defined_in_loop.insert(*l);
                } else if let StatementKind::VectorStore(
                    Operand::Copy(l) | Operand::Move(l),
                    _,
                    _,
                ) = &stmt.kind
                {
                    defined_in_loop.insert(*l);
                }
            }
        }

        let mut sorted_body: Vec<BasicBlockId> = lp.body.iter().copied().collect();
        sorted_body.sort_by_key(|id| id.0);

        let mut invariant_stmts = Vec::new();
        let mut invariant_locals = HashSet::default();
        let mut loop_changed = true;

        while loop_changed {
            loop_changed = false;
            for &bb_id in &sorted_body {
                let bb = &func.basic_blocks[bb_id.0];
                for (i, stmt) in bb.statements.iter().enumerate() {
                    if let StatementKind::Assign(local, rval) = &stmt.kind {
                        if func.local_decl(*local).is_some_and(|d| d.name.is_none())
                            && assign_counts.get(local) == Some(&1)
                            && !invariant_locals.contains(local)
                            && self.is_invariant(rval, &defined_in_loop, &invariant_locals)
                            && self.is_safe_to_hoist(rval)
                        {
                            invariant_locals.insert(*local);
                            invariant_stmts.push((bb_id, i));
                            loop_changed = true;
                        }
                    }
                }
            }
        }

        if !invariant_stmts.is_empty() {
            self.hoist_invariants(func, lp.header, &lp.body, invariant_stmts)
        } else {
            false
        }
    }

    fn is_invariant(
        &self,
        rval: &Rvalue,
        defined_in_loop: &HashSet<Local>,
        invariant_locals: &HashSet<Local>,
    ) -> bool {
        match rval {
            Rvalue::Use(op) | Rvalue::UnaryOp(_, op) => {
                self.is_op_invariant(op, defined_in_loop, invariant_locals)
            },
            Rvalue::BinaryOp(_, l, r) | Rvalue::GetIndex(l, r) => {
                self.is_op_invariant(l, defined_in_loop, invariant_locals)
                    && self.is_op_invariant(r, defined_in_loop, invariant_locals)
            },
            _ => false,
        }
    }

    fn is_op_invariant(
        &self,
        op: &Operand,
        defined_in_loop: &HashSet<Local>,
        invariant_locals: &HashSet<Local>,
    ) -> bool {
        match op {
            Operand::Constant(_) => true,
            Operand::Copy(l) | Operand::Move(l) => {
                !defined_in_loop.contains(l) || invariant_locals.contains(l)
            },
        }
    }

    fn is_safe_to_hoist(&self, rval: &Rvalue) -> bool {
        matches!(
            rval,
            Rvalue::Use(_)
                | Rvalue::UnaryOp(_, _)
                | Rvalue::BinaryOp(_, _, _)
                | Rvalue::GetIndex(_, _)
        )
    }

    fn hoist_invariants(
        &self,
        func: &mut MirFunction,
        header: BasicBlockId,
        body: &HashSet<BasicBlockId>,
        invariant_stmts: Vec<(BasicBlockId, usize)>,
    ) -> bool {
        let pre_header_id = BasicBlockId(func.basic_blocks.len());
        func.basic_blocks.push(BasicBlock {
            statements: Vec::new(),
            terminator: Some(Terminator {
                kind: TerminatorKind::Goto { target: header },
                span: Span::DUMMY,
            }),
        });

        let mut changed = false;
        for i in 0..func.basic_blocks.len() - 1 {
            let bb_id = BasicBlockId(i);
            if body.contains(&bb_id) {
                continue;
            }

            let bb = &mut func.basic_blocks[i];
            if let Some(term) = &mut bb.terminator {
                match &mut term.kind {
                    TerminatorKind::Goto { target } if *target == header => {
                        *target = pre_header_id;
                        changed = true;
                    },
                    TerminatorKind::SwitchInt { targets, otherwise, .. } => {
                        for (_, t) in targets {
                            if *t == header {
                                *t = pre_header_id;
                                changed = true;
                            }
                        }
                        if *otherwise == header {
                            *otherwise = pre_header_id;
                            changed = true;
                        }
                    },
                    _ => {},
                }
            }
        }

        if !changed {
            func.basic_blocks.pop();
            return false;
        }

        let mut stmts_to_move = Vec::new();
        for (bb_id, stmt_idx) in invariant_stmts {
            let stmt = func.basic_blocks[bb_id.0].statements[stmt_idx].clone();
            stmts_to_move.push((bb_id, stmt_idx, stmt));
        }

        let mut to_remove = stmts_to_move
            .iter()
            .map(|(bb, idx, _)| (*bb, *idx))
            .collect::<Vec<_>>();
        to_remove.sort_by(|a, b| {
            if a.0 != b.0 {
                a.0 .0.cmp(&b.0 .0)
            } else {
                b.1.cmp(&a.1)
            }
        });

        for (bb_id, idx) in to_remove {
            func.basic_blocks[bb_id.0].statements.remove(idx);
        }

        for (_, _, stmt) in stmts_to_move {
            func.basic_blocks[pre_header_id.0].statements.push(stmt);
        }

        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ling_ast::ast::BinOp;

    fn temp(name: Option<&str>) -> LocalDecl {
        LocalDecl {
            ty: MirType::Any,
            name: name.map(str::to_string),
            span: Span::DUMMY,
            is_mut: true,
            is_owning: true,
        }
    }

    fn stmt(kind: StatementKind) -> Statement {
        Statement { kind, span: Span::DUMMY }
    }

    fn goto(target: usize) -> Terminator {
        Terminator {
            kind: TerminatorKind::Goto { target: BasicBlockId(target) },
            span: Span::DUMMY,
        }
    }

    // A one-argument function whose loop body assigns the highest-indexed
    // temporary. The pre-fix code indexed `func.locals[local.0]` directly,
    // which panicked once a flat `Local` index reached `locals.len()`.
    #[test]
    fn hoists_invariant_without_indexing_past_locals() {
        let mut func = MirFunction::new("f", 1);
        // Flat space: Local(0)=return, Local(1)=param, Local(2..=4)=temps below.
        func.locals.push(temp(Some("acc"))); // Local(2)
        func.locals.push(temp(None)); // Local(3): loop-invariant temp
        func.locals.push(temp(None)); // Local(4): loop-variant accumulator

        func.basic_blocks = vec![
            // bb0: external pre-header, branches into the loop header.
            BasicBlock { statements: Vec::new(), terminator: Some(goto(1)) },
            // bb1: loop header.
            BasicBlock {
                statements: Vec::new(),
                terminator: Some(Terminator {
                    kind: TerminatorKind::SwitchInt {
                        discr: Operand::Copy(Local(2)),
                        targets: vec![(1, BasicBlockId(2))],
                        otherwise: BasicBlockId(3),
                    },
                    span: Span::DUMMY,
                }),
            },
            // bb2: loop body — invariant `param * param`, then a variant update.
            BasicBlock {
                statements: vec![
                    stmt(StatementKind::Assign(
                        Local(3),
                        Rvalue::BinaryOp(
                            BinOp::Mul,
                            Operand::Copy(Local(1)),
                            Operand::Copy(Local(1)),
                        ),
                    )),
                    stmt(StatementKind::Assign(
                        Local(4),
                        Rvalue::BinaryOp(
                            BinOp::Add,
                            Operand::Copy(Local(4)),
                            Operand::Move(Local(3)),
                        ),
                    )),
                ],
                terminator: Some(goto(1)),
            },
            // bb3: exit.
            BasicBlock {
                statements: vec![stmt(StatementKind::Assign(
                    Local(0),
                    Rvalue::Use(Operand::Copy(Local(4))),
                ))],
                terminator: Some(Terminator { kind: TerminatorKind::Return, span: Span::DUMMY }),
            },
        ];

        assert!(Licm.run(&mut func), "expected the invariant to be hoisted");

        let is_invariant_mul = |s: &Statement| {
            matches!(
                &s.kind,
                StatementKind::Assign(Local(3), Rvalue::BinaryOp(BinOp::Mul, ..))
            )
        };
        let body_has_mul = func.basic_blocks[2].statements.iter().any(is_invariant_mul);
        assert!(!body_has_mul, "invariant must leave the loop body");
        let total_mul: usize = func
            .basic_blocks
            .iter()
            .flat_map(|bb| &bb.statements)
            .filter(|s| is_invariant_mul(s))
            .count();
        assert_eq!(total_mul, 1, "invariant must be hoisted exactly once");
    }
}

← Back to index