Skip to main content

factorio_codegen/
generator.rs

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