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 let path = module_path.replace('.', "/");
180 let prefixed_path = self.apply_path_prefix(&path);
181 format!("__{}__/lua/{}", self.mod_name, prefixed_path)
182 }
183
184 #[must_use]
185 pub fn apply_path_prefix(&self, path: &str) -> String {
186 if self.module_prefix.is_empty() {
187 return path.to_string();
188 }
189 path.rfind('/').map_or_else(
190 || format!("{}_{}", self.module_prefix, path),
191 |slash| {
192 format!(
193 "{}/{prefix}_{}",
194 &path[..slash],
195 &path[slash + 1..],
196 prefix = self.module_prefix
197 )
198 },
199 )
200 }
201
202 #[must_use]
204 pub fn prefixed_local(&self, local: &str) -> String {
205 if self.module_prefix.is_empty() {
206 local.to_string()
207 } else {
208 format!("{}_{}", self.module_prefix, local)
209 }
210 }
211
212 pub fn generate_module(&mut self, module: &Module) -> LuaGeneratorResult<String> {
217 self.output.clear();
218 self.indent_level = 0;
219 self.exported_functions.clear();
220
221 for symbol in &module.symbols {
222 if let Statement::FunctionDecl(function) = &symbol.statement {
223 self.exported_functions.insert(function.name.clone());
224 }
225 }
226
227 let module_name = Self::module_identifier(&module.name);
228 self.current_module_table = Some(module_name.clone());
231
232 let module_header = self.generate_module_meta(module);
233 self.output.push_str(&module_header);
234 self.output.push('\n');
235
236 self.generate_imports(&module.imports);
237
238 for statement in &module.body.statements {
239 self.generate_statement(statement, Some(module), None, Scope::Private)?;
240 }
241
242 let (exporter_start, exporter_end) = Self::generate_symbol_exporter(&module_name);
243 self.write_line(&exporter_start);
244
245 if !module.submodules.is_empty() {
246 self.write_line(&format!(
247 "package.loaded[\"{}\"] = {module_name}",
248 self.mod_require_path(&module.name)
249 ));
250 }
251
252 for symbol in &module.symbols {
253 self.generate_statement(
254 &symbol.statement,
255 Some(module),
256 Some(&module_name),
257 symbol.scope,
258 )?;
259 }
260
261 self.generate_submodules(&module.submodules);
262
263 self.write_line(&exporter_end);
264 self.current_module_table = None;
265 self.exported_functions.clear();
266
267 Ok(self.output.clone())
268 }
269
270 fn generate_imports(&mut self, imports: &[factorio_ir::module::ModuleImport]) {
271 for import in imports {
272 self.write_line(&format!(
274 "local {} = require(\"{}\")",
275 import.local,
276 self.mod_require_path(&import.module)
277 ));
278
279 for item in &import.items {
280 self.write_line(&format!(
281 "local {} = {}.{}",
282 item.local, import.local, item.name
283 ));
284 }
285 }
286
287 if !imports.is_empty() {
288 self.output.push('\n');
289 }
290 }
291
292 fn generate_submodules(&mut self, submodules: &[String]) {
293 for submodule in submodules {
294 self.write_line(&format!(
295 "require(\"{}\")",
296 self.mod_require_path(submodule)
297 ));
298 }
299
300 if !submodules.is_empty() {
301 self.output.push('\n');
302 }
303 }
304
305 fn generate_symbol_exporter(module_name: &str) -> (String, String) {
307 (
308 format!("local {module_name} = {{}}"),
309 format!("return {module_name}"),
310 )
311 }
312
313 fn generate_block(&mut self, block: &Block, module: Option<&Module>) -> LuaGeneratorResult<()> {
314 for statement in &block.statements {
315 self.generate_statement(statement, module, None, Scope::Private)?;
316 }
317 Ok(())
318 }
319
320 fn generate_literal(literal: &Literal) -> String {
321 match literal {
322 Literal::Int(value) => value.to_string(),
323 Literal::Float(value) => value.to_string(),
324 Literal::String(value) => format!("\"{}\"", escape_lua_string(value)),
325 Literal::Bool(value) => value.to_string(),
326 Literal::Nil => "nil".to_string(),
327 }
328 }
329
330 fn format_parameter(&self, parameter: &factorio_ir::function::Parameter) -> String {
331 let type_comment = self.parameter_type_comment(parameter.source_type.as_deref());
332 format!("{}{type_comment}", parameter.name)
333 }
334}
335
336fn escape_lua_string(value: &str) -> String {
337 value
338 .replace('\\', "\\\\")
339 .replace('"', "\\\"")
340 .replace('\n', "\\n")
341 .replace('\r', "\\r")
342 .replace('\t', "\\t")
343}