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 struct_table_context: Option<(String, String)>,
20 debug_level: Option<u8>,
21 mod_name: String,
22 for_depth: usize,
24 module_prefix: String,
27 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 #[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 #[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 fn indent(&self) -> String {
107 Self::INDENT.repeat(self.indent_level)
108 }
109
110 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 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 #[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 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 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 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}