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        self.require_path(&self.mod_name, "lua", module_path, true)
180    }
181
182    fn import_require_path(&self, import: &factorio_ir::module::ModuleImport) -> String {
183        let own_mod = import.factorio_mod.is_none();
184        let mod_name = import
185            .factorio_mod
186            .as_deref()
187            .unwrap_or(self.mod_name.as_str());
188        let module_root = import.module_root.as_deref().unwrap_or("lua");
189        self.require_path(mod_name, module_root, &import.module, own_mod)
190    }
191
192    fn require_path(
193        &self,
194        mod_name: &str,
195        module_root: &str,
196        module_path: &str,
197        apply_prefix: bool,
198    ) -> String {
199        let path = module_path.replace('.', "/");
200        let path = if apply_prefix {
201            self.apply_path_prefix(&path)
202        } else {
203            path
204        };
205        if module_root.is_empty() {
206            format!("__{mod_name}__/{path}")
207        } else {
208            format!("__{mod_name}__/{module_root}/{path}")
209        }
210    }
211
212    #[must_use]
213    pub fn apply_path_prefix(&self, path: &str) -> String {
214        if self.module_prefix.is_empty() {
215            return path.to_string();
216        }
217        path.rfind('/').map_or_else(
218            || format!("{}_{}", self.module_prefix, path),
219            |slash| {
220                format!(
221                    "{}/{prefix}_{}",
222                    &path[..slash],
223                    &path[slash + 1..],
224                    prefix = self.module_prefix
225                )
226            },
227        )
228    }
229
230    /// Return the prefixed local variable name for a module import.
231    #[must_use]
232    pub fn prefixed_local(&self, local: &str) -> String {
233        if self.module_prefix.is_empty() {
234            local.to_string()
235        } else {
236            format!("{}_{}", self.module_prefix, local)
237        }
238    }
239
240    /// Generate lua code for a single `module`.
241    ///
242    /// # Errors
243    /// Returns `Err` if parsing the AST fails.
244    pub fn generate_module(&mut self, module: &Module) -> LuaGeneratorResult<String> {
245        self.output.clear();
246        self.indent_level = 0;
247        self.exported_functions.clear();
248
249        for symbol in &module.symbols {
250            if let Statement::FunctionDecl(function) = &symbol.statement {
251                self.exported_functions.insert(function.name.clone());
252            }
253        }
254
255        let module_name = Self::module_identifier(&module.name);
256        // Qualify exported fn references even in private body code; those bodies only
257        // run after the module table exists (events / commands), not during load.
258        self.current_module_table = Some(module_name.clone());
259
260        let module_header = self.generate_module_meta(module);
261        self.output.push_str(&module_header);
262        self.output.push('\n');
263
264        self.generate_imports(&module.imports);
265
266        for statement in &module.body.statements {
267            self.generate_statement(statement, Some(module), None, Scope::Private)?;
268        }
269
270        let (exporter_start, exporter_end) = Self::generate_symbol_exporter(&module_name);
271        self.write_line(&exporter_start);
272
273        if !module.submodules.is_empty() {
274            self.write_line(&format!(
275                "package.loaded[\"{}\"] = {module_name}",
276                self.mod_require_path(&module.name)
277            ));
278        }
279
280        for symbol in &module.symbols {
281            self.generate_statement(
282                &symbol.statement,
283                Some(module),
284                Some(&module_name),
285                symbol.scope,
286            )?;
287        }
288
289        self.generate_submodules(&module.submodules);
290
291        self.write_line(&exporter_end);
292        self.current_module_table = None;
293        self.exported_functions.clear();
294
295        Ok(self.output.clone())
296    }
297
298    fn generate_imports(&mut self, imports: &[factorio_ir::module::ModuleImport]) {
299        for import in imports {
300            // `import.local` already has the module prefix baked in at the IR level.
301            self.write_line(&format!(
302                "local {} = require(\"{}\")",
303                import.local,
304                self.import_require_path(import)
305            ));
306
307            for item in &import.items {
308                self.write_line(&format!(
309                    "local {} = {}.{}",
310                    item.local, import.local, item.name
311                ));
312            }
313        }
314
315        if !imports.is_empty() {
316            self.output.push('\n');
317        }
318    }
319
320    fn generate_submodules(&mut self, submodules: &[String]) {
321        for submodule in submodules {
322            self.write_line(&format!(
323                "require(\"{}\")",
324                self.mod_require_path(submodule)
325            ));
326        }
327
328        if !submodules.is_empty() {
329            self.output.push('\n');
330        }
331    }
332
333    /// Return the starting and ending lines for exporting symbols.
334    fn generate_symbol_exporter(module_name: &str) -> (String, String) {
335        (
336            format!("local {module_name} = {{}}"),
337            format!("return {module_name}"),
338        )
339    }
340
341    fn generate_block(&mut self, block: &Block, module: Option<&Module>) -> LuaGeneratorResult<()> {
342        for statement in &block.statements {
343            self.generate_statement(statement, module, None, Scope::Private)?;
344        }
345        Ok(())
346    }
347
348    fn generate_literal(literal: &Literal) -> String {
349        match literal {
350            Literal::Int(value) => value.to_string(),
351            Literal::Float(value) => value.to_string(),
352            Literal::String(value) => format!("\"{}\"", escape_lua_string(value)),
353            Literal::Bool(value) => value.to_string(),
354            Literal::Nil => "nil".to_string(),
355        }
356    }
357
358    fn format_parameter(&self, parameter: &factorio_ir::function::Parameter) -> String {
359        let type_comment = self.parameter_type_comment(parameter.source_type.as_deref());
360        format!("{}{type_comment}", parameter.name)
361    }
362}
363
364fn escape_lua_string(value: &str) -> String {
365    value
366        .replace('\\', "\\\\")
367        .replace('"', "\\\"")
368        .replace('\n', "\\n")
369        .replace('\r', "\\r")
370        .replace('\t', "\\t")
371}