Skip to main content

factorio_codegen/
generator.rs

1use std::fmt::Write as _;
2
3use factorio_ir::{
4    block::Block, literal::Literal, module::Module, scope::Scope, statement::Statement,
5};
6
7use crate::generator::error::LuaGeneratorResult;
8
9pub mod comment;
10pub mod error;
11pub mod expression;
12pub mod function;
13pub mod operator;
14pub mod statement;
15pub mod table;
16
17pub struct LuaGenerator {
18    output: String,
19    indent_level: usize,
20    /// Rewrites `StructName.associated` paths while generating struct methods.
21    struct_table_context: Option<(String, String)>,
22    debug_level: Option<u8>,
23    mod_name: String,
24    /// Depth of nested `for` / `while` loops, used for `::__continue_N::` labels.
25    loop_depth: usize,
26    /// Optional prefix prepended to every module's filename and local require variable.
27    /// Empty string means no prefix.
28    module_prefix: String,
29    /// Active transpile profile name (`debug`, `release`, ...), if known.
30    profile: Option<String>,
31    exported_functions: std::collections::HashSet<String>,
32    current_module_table: Option<String>,
33}
34
35impl Default for LuaGenerator {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl LuaGenerator {
42    #[must_use]
43    pub fn new() -> Self {
44        Self::with_mod_name("mod")
45    }
46
47    #[must_use]
48    pub fn with_mod_name(mod_name: impl Into<String>) -> Self {
49        Self {
50            output: String::new(),
51            indent_level: 0,
52            struct_table_context: None,
53            debug_level: None,
54            mod_name: mod_name.into(),
55            loop_depth: 0,
56            module_prefix: String::new(),
57            profile: None,
58            exported_functions: std::collections::HashSet::new(),
59            current_module_table: None,
60        }
61    }
62
63    #[must_use]
64    pub fn with_debug_level(debug_level: u8) -> Self {
65        Self {
66            output: String::new(),
67            indent_level: 0,
68            struct_table_context: None,
69            debug_level: Some(debug_level),
70            mod_name: "mod".to_string(),
71            loop_depth: 0,
72            module_prefix: String::new(),
73            profile: None,
74            exported_functions: std::collections::HashSet::new(),
75            current_module_table: None,
76        }
77    }
78
79    #[must_use]
80    pub fn with_mod_name_and_debug(mod_name: impl Into<String>, debug_level: u8) -> Self {
81        Self {
82            output: String::new(),
83            indent_level: 0,
84            struct_table_context: None,
85            debug_level: Some(debug_level),
86            mod_name: mod_name.into(),
87            loop_depth: 0,
88            module_prefix: String::new(),
89            profile: None,
90            exported_functions: std::collections::HashSet::new(),
91            current_module_table: None,
92        }
93    }
94
95    /// Set the module prefix applied to all generated module filenames and local names.
96    #[must_use]
97    pub fn with_module_prefix(mut self, prefix: impl Into<String>) -> Self {
98        self.module_prefix = prefix.into();
99        self
100    }
101
102    /// Record the transpile profile name in generated file headers.
103    #[must_use]
104    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
105        self.profile = Some(profile.into());
106        self
107    }
108
109    const fn debug_level_at_least(&self, level: u8) -> bool {
110        matches!(self.debug_level, Some(current) if current >= level)
111    }
112
113    /// Clone generator state into a scratch buffer for emitting nested expression forms
114    /// (e.g. closures) without touching the primary output.
115    fn fork_expr_emitter(&self) -> Self {
116        Self {
117            output: String::new(),
118            indent_level: 0,
119            struct_table_context: self.struct_table_context.clone(),
120            debug_level: self.debug_level,
121            mod_name: self.mod_name.clone(),
122            loop_depth: 0,
123            module_prefix: self.module_prefix.clone(),
124            profile: self.profile.clone(),
125            exported_functions: self.exported_functions.clone(),
126            current_module_table: self.current_module_table.clone(),
127        }
128    }
129
130    pub const INDENT: &'static str = "\t";
131
132    /// Returns a string indented to [`LuaGenerator::indent_level`].
133    fn indent(&self) -> String {
134        Self::INDENT.repeat(self.indent_level)
135    }
136
137    /// Writes a line of text to [`LuaGenerator::output`] terminated by a newline `\n`.
138    fn write_line(&mut self, line: &str) {
139        if !line.is_empty() {
140            self.output.push_str(&self.indent());
141            self.output.push_str(line);
142        }
143        self.output.push('\n');
144    }
145
146    fn write_doc_comments(&mut self, doc: Option<&str>) {
147        let Some(doc) = doc else {
148            return;
149        };
150
151        for line in doc.lines() {
152            if line.is_empty() {
153                self.write_line("--");
154            } else {
155                self.write_line(&format!("-- {line}"));
156            }
157        }
158    }
159
160    pub const MODULE_META_START: &'static str =
161        concat!("-- Generated by factorio-rs@", env!("CARGO_PKG_VERSION"));
162
163    fn generate_module_meta(&self, module: &Module) -> String {
164        let mut header = format!("{}\n", Self::MODULE_META_START);
165        if let Some(profile) = &self.profile {
166            let _ = writeln!(header, "-- Profile: {profile}");
167        }
168        let _ = write!(header, "-- Module: `{}`", module.name);
169        header
170    }
171
172    /// Returns the module identifier that exported symbols will be attached to, e.g
173    /// `bound_detector` -> `boundDetector`, `player.extra_info` -> `playerExtraInfo`.
174    fn module_identifier(module_name: &str) -> String {
175        heck::AsLowerCamelCase(module_name.replace('.', "_")).to_string()
176    }
177
178    fn mod_require_path(&self, module_path: &str) -> String {
179        let path = module_path.replace('.', "/");
180        let prefixed_path = self.apply_path_prefix(&path);
181        format!("__{}__/lua/{}", self.mod_name, prefixed_path)
182    }
183
184    #[must_use]
185    pub fn apply_path_prefix(&self, path: &str) -> String {
186        if self.module_prefix.is_empty() {
187            return path.to_string();
188        }
189        path.rfind('/').map_or_else(
190            || format!("{}_{}", self.module_prefix, path),
191            |slash| {
192                format!(
193                    "{}/{prefix}_{}",
194                    &path[..slash],
195                    &path[slash + 1..],
196                    prefix = self.module_prefix
197                )
198            },
199        )
200    }
201
202    /// Return the prefixed local variable name for a module import.
203    #[must_use]
204    pub fn prefixed_local(&self, local: &str) -> String {
205        if self.module_prefix.is_empty() {
206            local.to_string()
207        } else {
208            format!("{}_{}", self.module_prefix, local)
209        }
210    }
211
212    /// Generate lua code for a single `module`.
213    ///
214    /// # Errors
215    /// Returns `Err` if parsing the AST fails.
216    pub fn generate_module(&mut self, module: &Module) -> LuaGeneratorResult<String> {
217        self.output.clear();
218        self.indent_level = 0;
219        self.exported_functions.clear();
220
221        for symbol in &module.symbols {
222            if let Statement::FunctionDecl(function) = &symbol.statement {
223                self.exported_functions.insert(function.name.clone());
224            }
225        }
226
227        let module_name = Self::module_identifier(&module.name);
228        // Qualify exported fn references even in private body code; those bodies only
229        // run after the module table exists (events / commands), not during load.
230        self.current_module_table = Some(module_name.clone());
231
232        let module_header = self.generate_module_meta(module);
233        self.output.push_str(&module_header);
234        self.output.push('\n');
235
236        self.generate_imports(&module.imports);
237
238        for statement in &module.body.statements {
239            self.generate_statement(statement, Some(module), None, Scope::Private)?;
240        }
241
242        let (exporter_start, exporter_end) = Self::generate_symbol_exporter(&module_name);
243        self.write_line(&exporter_start);
244
245        if !module.submodules.is_empty() {
246            self.write_line(&format!(
247                "package.loaded[\"{}\"] = {module_name}",
248                self.mod_require_path(&module.name)
249            ));
250        }
251
252        for symbol in &module.symbols {
253            self.generate_statement(
254                &symbol.statement,
255                Some(module),
256                Some(&module_name),
257                symbol.scope,
258            )?;
259        }
260
261        self.generate_submodules(&module.submodules);
262
263        self.write_line(&exporter_end);
264        self.current_module_table = None;
265        self.exported_functions.clear();
266
267        Ok(self.output.clone())
268    }
269
270    fn generate_imports(&mut self, imports: &[factorio_ir::module::ModuleImport]) {
271        for import in imports {
272            // `import.local` already has the module prefix baked in at the IR level.
273            self.write_line(&format!(
274                "local {} = require(\"{}\")",
275                import.local,
276                self.mod_require_path(&import.module)
277            ));
278
279            for item in &import.items {
280                self.write_line(&format!(
281                    "local {} = {}.{}",
282                    item.local, import.local, item.name
283                ));
284            }
285        }
286
287        if !imports.is_empty() {
288            self.output.push('\n');
289        }
290    }
291
292    fn generate_submodules(&mut self, submodules: &[String]) {
293        for submodule in submodules {
294            self.write_line(&format!(
295                "require(\"{}\")",
296                self.mod_require_path(submodule)
297            ));
298        }
299
300        if !submodules.is_empty() {
301            self.output.push('\n');
302        }
303    }
304
305    /// Return the starting and ending lines for exporting symbols.
306    fn generate_symbol_exporter(module_name: &str) -> (String, String) {
307        (
308            format!("local {module_name} = {{}}"),
309            format!("return {module_name}"),
310        )
311    }
312
313    fn generate_block(&mut self, block: &Block, module: Option<&Module>) -> LuaGeneratorResult<()> {
314        for statement in &block.statements {
315            self.generate_statement(statement, module, None, Scope::Private)?;
316        }
317        Ok(())
318    }
319
320    fn generate_literal(literal: &Literal) -> String {
321        match literal {
322            Literal::Int(value) => value.to_string(),
323            Literal::Float(value) => value.to_string(),
324            Literal::String(value) => format!("\"{}\"", escape_lua_string(value)),
325            Literal::Bool(value) => value.to_string(),
326            Literal::Nil => "nil".to_string(),
327        }
328    }
329
330    fn format_parameter(&self, parameter: &factorio_ir::function::Parameter) -> String {
331        let type_comment = self.parameter_type_comment(parameter.source_type.as_deref());
332        format!("{}{type_comment}", parameter.name)
333    }
334}
335
336fn escape_lua_string(value: &str) -> String {
337    value
338        .replace('\\', "\\\\")
339        .replace('"', "\\\"")
340        .replace('\n', "\\n")
341        .replace('\r', "\\r")
342        .replace('\t', "\\t")
343}