Skip to main content

factorio_ir/prune/
mod.rs

1//! Dead-code elimination for lowered IR modules.
2//!
3//! Pruning runs after lowering and before Lua codegen. Reachability starts from
4//! event handlers registered with `#[factorio_rs::event]`, then follows call graphs
5//! within and across modules. Anything not reached is removed from the IR so it
6//! is never emitted to Lua.
7
8mod apply;
9mod items;
10mod module_graph;
11mod reachability;
12mod references;
13mod struct_utils;
14
15use crate::module::Module;
16
17use self::{apply::prune_module, module_graph::ModuleGraph, reachability::compute_reachability};
18
19/// Remove unreachable functions and exports from transpiled modules.
20///
21/// When dead-code pruning is enabled for the active transpile profile, the build
22/// collects all lowered modules, runs this pass, then generates Lua from the pruned IR.
23pub fn prune_modules(modules: &mut [Module]) {
24    if modules.is_empty() {
25        return;
26    }
27
28    let graph = ModuleGraph::new(modules);
29    let reachability = compute_reachability(&graph);
30
31    for module in modules {
32        if let Some(reach) = reachability.get(&module.name) {
33            prune_module(module, reach);
34        }
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use crate::{
41        block::Block,
42        function::Function,
43        module::{Module, Symbol},
44        scope::Scope,
45        stage::Stage,
46        statement::Statement,
47    };
48
49    use super::prune_modules;
50
51    #[test]
52    fn prunes_unreachable_private_functions() {
53        let mut modules = vec![Module {
54            name: "control".to_string(),
55            stage: Stage::Control,
56            body: Block {
57                statements: vec![Statement::FunctionDecl(Function {
58                    name: "add".to_string(),
59                    params: vec![],
60                    body: Block { statements: vec![] },
61                    doc: None,
62                    debug: None,
63                    event: None,
64                    event_filter: None,
65                })],
66            },
67            imports: vec![],
68            submodules: vec![],
69            locales: vec![],
70            symbols: vec![Symbol {
71                scope: Scope::Public,
72                statement: Statement::FunctionDecl(Function {
73                    name: "on_init".to_string(),
74                    params: vec![],
75                    body: Block { statements: vec![] },
76                    doc: None,
77                    debug: None,
78                    event: Some("on_init".to_string()),
79                    event_filter: None,
80                }),
81            }],
82        }];
83
84        prune_modules(&mut modules);
85
86        assert!(modules[0].body.statements.is_empty());
87        assert_eq!(modules[0].symbols.len(), 1);
88        assert_eq!(
89            match &modules[0].symbols[0].statement {
90                Statement::FunctionDecl(function) => function.name.as_str(),
91                _ => panic!("expected function"),
92            },
93            "on_init"
94        );
95    }
96
97    #[test]
98    fn prunes_unused_public_exports() {
99        let mut modules = vec![Module {
100            name: "control".to_string(),
101            stage: Stage::Control,
102            body: Block { statements: vec![] },
103            imports: vec![],
104            submodules: vec![],
105            locales: vec![],
106            symbols: vec![
107                Symbol {
108                    scope: Scope::Public,
109                    statement: Statement::FunctionDecl(Function {
110                        name: "unused".to_string(),
111                        params: vec![],
112                        body: Block { statements: vec![] },
113                        doc: None,
114                        debug: None,
115                        event: None,
116                        event_filter: None,
117                    }),
118                },
119                Symbol {
120                    scope: Scope::Public,
121                    statement: Statement::FunctionDecl(Function {
122                        name: "on_init".to_string(),
123                        params: vec![],
124                        body: Block { statements: vec![] },
125                        doc: None,
126                        debug: None,
127                        event: Some("on_init".to_string()),
128                        event_filter: None,
129                    }),
130                },
131            ],
132        }];
133
134        prune_modules(&mut modules);
135
136        assert_eq!(modules[0].symbols.len(), 1);
137    }
138}