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    /// Locals forward-declared before vtables so closures capture upvalues.
34    forward_declared_locals: std::collections::HashSet<String>,
35}
36
37impl Default for LuaGenerator {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl LuaGenerator {
44    #[must_use]
45    pub fn new() -> Self {
46        Self::with_mod_name("mod")
47    }
48
49    #[must_use]
50    pub fn with_mod_name(mod_name: impl Into<String>) -> Self {
51        Self {
52            output: String::new(),
53            indent_level: 0,
54            struct_table_context: None,
55            debug_level: None,
56            mod_name: mod_name.into(),
57            loop_depth: 0,
58            module_prefix: String::new(),
59            profile: None,
60            exported_functions: std::collections::HashSet::new(),
61            current_module_table: None,
62            forward_declared_locals: std::collections::HashSet::new(),
63        }
64    }
65
66    #[must_use]
67    pub fn with_debug_level(debug_level: u8) -> Self {
68        Self {
69            output: String::new(),
70            indent_level: 0,
71            struct_table_context: None,
72            debug_level: Some(debug_level),
73            mod_name: "mod".to_string(),
74            loop_depth: 0,
75            module_prefix: String::new(),
76            profile: None,
77            exported_functions: std::collections::HashSet::new(),
78            current_module_table: None,
79            forward_declared_locals: std::collections::HashSet::new(),
80        }
81    }
82
83    #[must_use]
84    pub fn with_mod_name_and_debug(mod_name: impl Into<String>, debug_level: u8) -> Self {
85        Self {
86            output: String::new(),
87            indent_level: 0,
88            struct_table_context: None,
89            debug_level: Some(debug_level),
90            mod_name: mod_name.into(),
91            loop_depth: 0,
92            module_prefix: String::new(),
93            profile: None,
94            exported_functions: std::collections::HashSet::new(),
95            current_module_table: None,
96            forward_declared_locals: std::collections::HashSet::new(),
97        }
98    }
99
100    /// Set the module prefix applied to all generated module filenames and local names.
101    #[must_use]
102    pub fn with_module_prefix(mut self, prefix: impl Into<String>) -> Self {
103        self.module_prefix = prefix.into();
104        self
105    }
106
107    /// Record the transpile profile name in generated file headers.
108    #[must_use]
109    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
110        self.profile = Some(profile.into());
111        self
112    }
113
114    const fn debug_level_at_least(&self, level: u8) -> bool {
115        matches!(self.debug_level, Some(current) if current >= level)
116    }
117
118    /// Clone generator state into a scratch buffer for emitting nested expression forms
119    /// (e.g. closures) without touching the primary output.
120    fn fork_expr_emitter(&self) -> Self {
121        Self {
122            output: String::new(),
123            indent_level: 0,
124            struct_table_context: self.struct_table_context.clone(),
125            debug_level: self.debug_level,
126            mod_name: self.mod_name.clone(),
127            loop_depth: 0,
128            module_prefix: self.module_prefix.clone(),
129            profile: self.profile.clone(),
130            exported_functions: self.exported_functions.clone(),
131            current_module_table: self.current_module_table.clone(),
132            forward_declared_locals: self.forward_declared_locals.clone(),
133        }
134    }
135
136    pub const INDENT: &'static str = "\t";
137
138    /// Returns a string indented to [`LuaGenerator::indent_level`].
139    fn indent(&self) -> String {
140        Self::INDENT.repeat(self.indent_level)
141    }
142
143    /// Writes a line of text to [`LuaGenerator::output`] terminated by a newline `\n`.
144    fn write_line(&mut self, line: &str) {
145        if !line.is_empty() {
146            self.output.push_str(&self.indent());
147            self.output.push_str(line);
148        }
149        self.output.push('\n');
150    }
151
152    fn write_doc_comments(&mut self, doc: Option<&str>) {
153        let Some(doc) = doc else {
154            return;
155        };
156
157        for line in doc.lines() {
158            if line.is_empty() {
159                self.write_line("--");
160            } else {
161                self.write_line(&format!("-- {line}"));
162            }
163        }
164    }
165
166    pub const MODULE_META_START: &'static str =
167        concat!("-- Generated by factorio-rs@", env!("CARGO_PKG_VERSION"));
168
169    fn generate_module_meta(&self, module: &Module) -> String {
170        let mut header = format!("{}\n", Self::MODULE_META_START);
171        if let Some(profile) = &self.profile {
172            let _ = writeln!(header, "-- Profile: {profile}");
173        }
174        let _ = write!(header, "-- Module: `{}`", module.name);
175        header
176    }
177
178    /// Returns the module identifier that exported symbols will be attached to, e.g
179    /// `bound_detector` -> `boundDetector`, `player.extra_info` -> `playerExtraInfo`.
180    fn module_identifier(module_name: &str) -> String {
181        heck::AsLowerCamelCase(module_name.replace('.', "_")).to_string()
182    }
183
184    fn mod_require_path(&self, module_path: &str) -> String {
185        self.require_path(&self.mod_name, "lua", module_path, true)
186    }
187
188    fn import_require_path(&self, import: &factorio_ir::module::ModuleImport) -> String {
189        let own_mod = import.factorio_mod.is_none();
190        let mod_name = import
191            .factorio_mod
192            .as_deref()
193            .unwrap_or(self.mod_name.as_str());
194        let module_root = import.module_root.as_deref().unwrap_or("lua");
195        self.require_path(mod_name, module_root, &import.module, own_mod)
196    }
197
198    fn require_path(
199        &self,
200        mod_name: &str,
201        module_root: &str,
202        module_path: &str,
203        apply_prefix: bool,
204    ) -> String {
205        let path = module_path.replace('.', "/");
206        let path = if apply_prefix {
207            self.apply_path_prefix(&path)
208        } else {
209            path
210        };
211        if module_root.is_empty() {
212            format!("__{mod_name}__/{path}")
213        } else {
214            format!("__{mod_name}__/{module_root}/{path}")
215        }
216    }
217
218    #[must_use]
219    pub fn apply_path_prefix(&self, path: &str) -> String {
220        if self.module_prefix.is_empty() {
221            return path.to_string();
222        }
223        path.rfind('/').map_or_else(
224            || format!("{}_{}", self.module_prefix, path),
225            |slash| {
226                format!(
227                    "{}/{prefix}_{}",
228                    &path[..slash],
229                    &path[slash + 1..],
230                    prefix = self.module_prefix
231                )
232            },
233        )
234    }
235
236    /// Return the prefixed local variable name for a module import.
237    #[must_use]
238    pub fn prefixed_local(&self, local: &str) -> String {
239        if self.module_prefix.is_empty() {
240            local.to_string()
241        } else {
242            format!("{}_{}", self.module_prefix, local)
243        }
244    }
245
246    /// Generate lua code for a single `module`.
247    ///
248    /// # Errors
249    /// Returns `Err` if parsing the AST fails.
250    pub fn generate_module(&mut self, module: &Module) -> LuaGeneratorResult<String> {
251        self.output.clear();
252        self.indent_level = 0;
253        self.exported_functions.clear();
254        self.forward_declared_locals.clear();
255
256        for symbol in &module.symbols {
257            if let Statement::FunctionDecl(function) = &symbol.statement {
258                self.exported_functions.insert(function.name.clone());
259            }
260        }
261
262        let module_name = Self::module_identifier(&module.name);
263        // Qualify exported fn references even in private body code; those bodies only
264        // run after the module table exists (events / commands), not during load.
265        self.current_module_table = Some(module_name.clone());
266
267        let module_header = self.generate_module_meta(module);
268        self.output.push_str(&module_header);
269        self.output.push('\n');
270
271        self.generate_imports(&module.imports);
272
273        // Forward-declare private concrete type locals so vtable closures capture
274        // upvalues when structs are assigned later in the body.
275        self.generate_vtables(&module.vtables, Some(&module_name), module);
276
277        for statement in &module.body.statements {
278            self.generate_statement(statement, Some(module), None, Scope::Private)?;
279        }
280
281        let (exporter_start, exporter_end) = Self::generate_symbol_exporter(&module_name);
282        self.write_line(&exporter_start);
283
284        if !module.submodules.is_empty() {
285            self.write_line(&format!(
286                "package.loaded[\"{}\"] = {module_name}",
287                self.mod_require_path(&module.name)
288            ));
289        }
290
291        for symbol in &module.symbols {
292            self.generate_statement(
293                &symbol.statement,
294                Some(module),
295                Some(&module_name),
296                symbol.scope,
297            )?;
298        }
299
300        self.generate_submodules(&module.submodules);
301
302        self.write_line(&exporter_end);
303        self.current_module_table = None;
304        self.exported_functions.clear();
305
306        Ok(self.output.clone())
307    }
308
309    fn generate_imports(&mut self, imports: &[factorio_ir::module::ModuleImport]) {
310        for import in imports {
311            // `import.local` already has the module prefix baked in at the IR level.
312            self.write_line(&format!(
313                "local {} = require(\"{}\")",
314                import.local,
315                self.import_require_path(import)
316            ));
317
318            for item in &import.items {
319                self.write_line(&format!(
320                    "local {} = {}.{}",
321                    item.local, import.local, item.name
322                ));
323            }
324        }
325
326        if !imports.is_empty() {
327            self.output.push('\n');
328        }
329    }
330
331    fn generate_vtables(
332        &mut self,
333        vtables: &[factorio_ir::module::VTable],
334        module_name: Option<&str>,
335        module: &Module,
336    ) {
337        let mut forward = std::collections::BTreeSet::new();
338        for vtable in vtables {
339            if !concrete_type_is_public(module, &vtable.concrete_type) {
340                forward.insert(vtable.concrete_type.clone());
341            }
342        }
343        if !forward.is_empty() {
344            let names = forward.iter().cloned().collect::<Vec<_>>().join(", ");
345            self.write_line(&format!("local {names}"));
346            self.forward_declared_locals.extend(forward);
347            self.output.push('\n');
348        }
349
350        for vtable in vtables {
351            let concrete_path =
352                resolve_concrete_table_path(module, module_name, &vtable.concrete_type);
353            self.write_line(&format!("local {} = {{", vtable.name));
354            self.indent_level += 1;
355            for method in &vtable.methods {
356                self.write_line(&format!(
357                    "{method} = function(self, ...) return {concrete_path}.{method}(self._data, ...) end,"
358                ));
359            }
360            self.indent_level -= 1;
361            self.write_line("}");
362        }
363        if !vtables.is_empty() {
364            self.output.push('\n');
365        }
366    }
367
368    fn generate_submodules(&mut self, submodules: &[String]) {
369        for submodule in submodules {
370            self.write_line(&format!(
371                "require(\"{}\")",
372                self.mod_require_path(submodule)
373            ));
374        }
375
376        if !submodules.is_empty() {
377            self.output.push('\n');
378        }
379    }
380
381    /// Return the starting and ending lines for exporting symbols.
382    fn generate_symbol_exporter(module_name: &str) -> (String, String) {
383        (
384            format!("local {module_name} = {{}}"),
385            format!("return {module_name}"),
386        )
387    }
388
389    fn generate_block(&mut self, block: &Block, module: Option<&Module>) -> LuaGeneratorResult<()> {
390        for statement in &block.statements {
391            self.generate_statement(statement, module, None, Scope::Private)?;
392        }
393        Ok(())
394    }
395
396    fn generate_literal(literal: &Literal) -> String {
397        match literal {
398            Literal::Int(value) => value.to_string(),
399            Literal::Float(value) => value.to_string(),
400            Literal::String(value) => format!("\"{}\"", escape_lua_string(value)),
401            Literal::Bool(value) => value.to_string(),
402            Literal::Nil => "nil".to_string(),
403        }
404    }
405
406    fn format_parameter(&self, parameter: &factorio_ir::function::Parameter) -> String {
407        let type_comment = self.parameter_type_comment(parameter.source_type.as_deref());
408        format!("{}{type_comment}", parameter.name)
409    }
410}
411
412fn escape_lua_string(value: &str) -> String {
413    value
414        .replace('\\', "\\\\")
415        .replace('"', "\\\"")
416        .replace('\n', "\\n")
417        .replace('\r', "\\r")
418        .replace('\t', "\\t")
419}
420
421fn concrete_type_is_public(module: &Module, concrete_type: &str) -> bool {
422    module.symbols.iter().any(|symbol| {
423        matches!(
424            &symbol.statement,
425            Statement::StructDecl(s) if s.name == concrete_type
426        ) || matches!(
427            &symbol.statement,
428            Statement::EnumDecl(e) if e.name == concrete_type
429        )
430    })
431}
432
433fn resolve_concrete_table_path(
434    module: &Module,
435    module_name: Option<&str>,
436    concrete_type: &str,
437) -> String {
438    if concrete_type_is_public(module, concrete_type)
439        && let Some(module_name) = module_name
440    {
441        format!("{module_name}.{concrete_type}")
442    } else {
443        concrete_type.to_string()
444    }
445}