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 struct_table_context: Option<(String, String)>,
22 debug_level: Option<u8>,
23 mod_name: String,
24 loop_depth: usize,
26 module_prefix: String,
29 profile: Option<String>,
31 exported_functions: std::collections::HashSet<String>,
32 current_module_table: Option<String>,
33 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 #[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 #[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 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 fn indent(&self) -> String {
140 Self::INDENT.repeat(self.indent_level)
141 }
142
143 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 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 #[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 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 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 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 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 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}