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}
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 #[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 #[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 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 fn indent(&self) -> String {
134 Self::INDENT.repeat(self.indent_level)
135 }
136
137 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 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 #[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 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 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 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 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}