Skip to main content

factorio_codegen/generator/
expression.rs

1use std::fmt::Write as _;
2
3use factorio_ir::expression::Expression;
4
5use crate::{LuaGenerator, attribute_property_for_setter};
6
7/// Map a Rust prototype struct name to its fixed Factorio `type` discriminant string.
8/// Returns `None` for non-prototype structs.
9fn prototype_lua_type(struct_name: &str) -> Option<&'static str> {
10    match struct_name {
11        "BoolSetting" => Some("bool-setting"),
12        "IntSetting" => Some("int-setting"),
13        "DoubleSetting" => Some("double-setting"),
14        "StringSetting" => Some("string-setting"),
15        "Item" | "RecipeIngredient" | "RecipeProduct" => Some("item"),
16        "Recipe" => Some("recipe"),
17        _ => None,
18    }
19}
20
21impl LuaGenerator {
22    #[must_use]
23    pub fn generate_expression(&self, expression: &Expression) -> String {
24        self.generate_expression_prec(expression, 0)
25    }
26
27    pub(crate) fn generate_expression_prec(&self, expression: &Expression, min_prec: u8) -> String {
28        match expression {
29            Expression::BinaryOp { lhs, op, rhs } => {
30                // `0 - x` (or `0.0 - x`) is the frontend's encoding of unary negation.
31                // Emit as Lua `-x` directly.
32                let is_zero_lhs = match lhs.as_ref() {
33                    Expression::Literal(factorio_ir::literal::Literal::Int(0)) => true,
34                    Expression::Literal(factorio_ir::literal::Literal::Float(f)) => *f == 0.0,
35                    _ => false,
36                };
37                if matches!(op, factorio_ir::operator::Operator::Sub) && is_zero_lhs {
38                    let rhs_str = self.generate_expression_prec(rhs, 100);
39                    let result = format!("-{rhs_str}");
40                    return if 0 < min_prec {
41                        format!("({result})")
42                    } else {
43                        result
44                    };
45                }
46
47                let prec = Self::operator_precedence(*op);
48                let lhs_str = self.generate_expression_prec(lhs, prec);
49                let rhs_str = self.generate_expression_prec(rhs, prec.saturating_add(1));
50                let result = format!("{} {} {}", lhs_str, Self::generate_operator(*op), rhs_str);
51
52                if prec < min_prec {
53                    format!("({result})")
54                } else {
55                    result
56                }
57            }
58            _ => self.generate_atom(expression),
59        }
60    }
61
62    /// Generate the smallest level of code (an atom).
63    pub(crate) fn generate_atom(&self, expression: &Expression) -> String {
64        match expression {
65            Expression::Literal(literal) => Self::generate_literal(literal),
66            Expression::Identifier(name) => self.generate_identifier(name),
67            Expression::FieldAccess { base, field } => {
68                let base = self.generate_expression(base);
69                format!("{base}.{field}")
70            }
71            Expression::QualifiedPath { segments } => self.generate_qualified_path(segments),
72            Expression::Call { func, args } => self.generate_call(func, args),
73            Expression::MethodCall {
74                receiver,
75                method,
76                args,
77            } => self.generate_method_call(receiver, method, args),
78            Expression::StructLiteral {
79                struct_name,
80                fields,
81            } => self.generate_struct_literal(struct_name.as_deref(), fields),
82            Expression::EnumLiteral {
83                enum_name,
84                variant,
85                fields,
86            } => self.generate_enum_literal(enum_name, variant, fields),
87            Expression::FormatConcat { parts } => parts
88                .iter()
89                .map(|part| self.generate_expression(part))
90                .collect::<Vec<_>>()
91                .join(" .. "),
92            Expression::Array { elements } => {
93                let elements = elements
94                    .iter()
95                    .map(|element| self.generate_expression(element))
96                    .collect::<Vec<_>>()
97                    .join(", ");
98                format!("{{ {elements} }}")
99            }
100            Expression::Index { base, key } => self.generate_index(base, key),
101            Expression::Not(inner) => self.generate_not(inner),
102            Expression::Len(inner) => {
103                let inner = self.generate_expression(inner);
104                format!("#{inner}")
105            }
106            Expression::If {
107                condition,
108                then_expr,
109                else_expr,
110            } => self.generate_if_expr(condition, then_expr, else_expr),
111            Expression::Closure { params, body } => self.generate_closure(params, body),
112            Expression::BinaryOp { .. } => {
113                unreachable!("binary operators are handled by generate_expression_prec")
114            }
115        }
116    }
117
118    /// Resolve a bare name: exported `pub fn`s become `module.name`, locals stay bare.
119    fn generate_identifier(&self, name: &str) -> String {
120        if self.exported_functions.contains(name)
121            && let Some(module_table) = &self.current_module_table
122        {
123            return format!("{module_table}.{name}");
124        }
125        name.to_string()
126    }
127
128    fn generate_qualified_path(&self, segments: &[String]) -> String {
129        if let Some((struct_name, table_path)) = &self.struct_table_context
130            && segments
131                .first()
132                .is_some_and(|segment| segment == struct_name)
133        {
134            let suffix = segments
135                .get(1..)
136                .map_or_else(String::new, |rest| rest.join("."));
137            if suffix.is_empty() {
138                return table_path.clone();
139            }
140            return format!("{table_path}.{suffix}");
141        }
142
143        segments.join(".")
144    }
145
146    fn generate_call(&self, func: &Expression, args: &[Expression]) -> String {
147        if let Expression::QualifiedPath { segments } = func
148            && args.is_empty()
149            && segments
150                .last()
151                .is_some_and(|s| s == "new" || s == "default")
152        {
153            match segments[0].as_str() {
154                "LuaAny" => return "nil".to_string(),
155                "Vec" if segments.last().is_some_and(|s| s == "new") => {
156                    return "{}".to_string();
157                }
158                _ if segments.last().is_some_and(|s| s == "default") => {
159                    return "{}".to_string();
160                }
161                _ => {}
162            }
163        }
164
165        let func_is_closure = matches!(func, Expression::Closure { .. });
166        let func = self.generate_expression(func);
167        let args = self.generate_arg_list(args);
168        if func_is_closure {
169            format!("({func})({args})")
170        } else {
171            format!("{func}({args})")
172        }
173    }
174
175    fn generate_method_call(
176        &self,
177        receiver: &Expression,
178        method: &str,
179        args: &[Expression],
180    ) -> String {
181        // `storage.get(key)` -> `storage[key]` (missing -> nil / Option::None).
182        // Must run before the settings `.get` rewrite (`recv[key].value`).
183        if method == "get" && args.len() == 1 && is_storage_receiver(receiver) {
184            let receiver = self.generate_expression(receiver);
185            let key = self.generate_expression(&args[0]);
186            return format!("{receiver}[{key}]");
187        }
188
189        if method == "get" && args.len() == 1 {
190            let receiver = self.generate_expression(receiver);
191            let key = self.generate_expression(&args[0]);
192            return format!("{receiver}[{key}].value");
193        }
194
195        // `storage.set(key, value)` -> `storage[key] = value` (Factorio persistent table).
196        if method == "set" && args.len() == 2 && is_storage_receiver(receiver) {
197            let receiver = self.generate_expression(receiver);
198            let key = self.generate_expression(&args[0]);
199            let value = self.generate_expression(&args[1]);
200            return format!("{receiver}[{key}] = {value}");
201        }
202
203        if method == "len" && args.is_empty() {
204            let receiver = self.generate_expression(receiver);
205            return format!("#{receiver}");
206        }
207
208        if method == "push" && args.len() == 1 {
209            let receiver = self.generate_expression(receiver);
210            let item = self.generate_expression(&args[0]);
211            return format!("table.insert({receiver}, {item})");
212        }
213
214        if method == "is_empty" && args.is_empty() {
215            let receiver = self.generate_expression(receiver);
216            return format!("#{receiver} == 0");
217        }
218
219        let trimmed = trim_trailing_nils(args);
220        if trimmed.is_empty() {
221            let receiver = self.generate_expression(receiver);
222            // Zero-arg API attributes are property reads (`entity.surface`).
223            // Calls that only passed trailing `None`s stay invocations (`entity.die()`).
224            if args.is_empty() {
225                return format!("{receiver}.{method}");
226            }
227            return format!("{receiver}.{method}()");
228        }
229
230        // Attribute writers (`set_caption` / `write_driving`) -> property assign.
231        // Real Factorio `set_*` methods and user methods are absent from the lookup.
232        if trimmed.len() == 1
233            && let Some(property) = attribute_property_for_setter(method)
234        {
235            let receiver = self.generate_expression(receiver);
236            let value = self.generate_expression(&trimmed[0]);
237            return format!("{receiver}.{property} = {value}");
238        }
239
240        let receiver = self.generate_expression(receiver);
241        let args_lua = self.generate_arg_list(trimmed);
242        format!("{receiver}.{method}({args_lua})")
243    }
244
245    /// Join call arguments, omitting trailing `nil` so Factorio optional params stay unset.
246    fn generate_arg_list(&self, args: &[Expression]) -> String {
247        trim_trailing_nils(args)
248            .iter()
249            .map(|arg| self.generate_expression(arg))
250            .collect::<Vec<_>>()
251            .join(", ")
252    }
253
254    fn generate_struct_literal(
255        &self,
256        struct_name: Option<&str>,
257        fields: &[(String, Expression)],
258    ) -> String {
259        let injected_type = struct_name.and_then(prototype_lua_type);
260        let type_prefix = injected_type.map(|t| format!("type = \"{t}\", "));
261
262        let field_strs = fields
263            .iter()
264            .filter(|(name, value)| {
265                if matches!(
266                    value,
267                    Expression::Literal(factorio_ir::literal::Literal::Nil)
268                ) {
269                    return false;
270                }
271                injected_type.is_none() || (name != "type" && name != "r#type")
272            })
273            .map(|(name, value)| {
274                let lua_name = if name == "r#type" {
275                    "type"
276                } else {
277                    name.as_str()
278                };
279                format!("{lua_name} = {}", self.generate_expression(value))
280            })
281            .collect::<Vec<_>>()
282            .join(", ");
283
284        let inner = match type_prefix {
285            Some(prefix) if !field_strs.is_empty() => format!("{prefix}{field_strs}"),
286            Some(prefix) => prefix.trim_end_matches(", ").to_string(),
287            None => field_strs,
288        };
289        let literal = format!("{{ {inner} }}");
290
291        if let Some((_, table_path)) = &self.struct_table_context {
292            format!("setmetatable({literal}, {{ __index = {table_path} }})")
293        } else {
294            literal
295        }
296    }
297
298    fn generate_enum_literal(
299        &self,
300        enum_name: &str,
301        variant: &str,
302        fields: &[(String, Expression)],
303    ) -> String {
304        let mut parts = vec![format!("tag = \"{variant}\"")];
305        parts.extend(
306            fields
307                .iter()
308                .map(|(name, value)| format!("{name} = {}", self.generate_expression(value))),
309        );
310        let literal = format!("{{ {} }}", parts.join(", "));
311        if let Some((name, table_path)) = &self.struct_table_context
312            && name == enum_name
313        {
314            format!("setmetatable({literal}, {{ __index = {table_path} }})")
315        } else {
316            literal
317        }
318    }
319
320    fn generate_index(&self, base: &Expression, key: &Expression) -> String {
321        let base = self.generate_expression(base);
322
323        // Lua is 1-indexed: shift Rust integer literals (`0` -> `1`, `1` -> `2`, ...).
324        // Variable indices are left as-is (callers should use 1-based values).
325        let key = match key {
326            Expression::Literal(factorio_ir::literal::Literal::Int(index)) => {
327                (*index + 1).to_string()
328            }
329            _ => self.generate_expression(key),
330        };
331        format!("{base}[{key}]")
332    }
333
334    fn generate_not(&self, inner: &Expression) -> String {
335        if let Expression::MethodCall {
336            receiver,
337            method,
338            args,
339        } = inner
340            && method == "is_empty"
341            && args.is_empty()
342        {
343            let receiver = self.generate_expression(receiver);
344            return format!("#{receiver} ~= 0");
345        }
346
347        let needs_parens = matches!(inner, Expression::BinaryOp { .. });
348        let inner_str = self.generate_expression(inner);
349        if needs_parens {
350            format!("not ({inner_str})")
351        } else {
352            format!("not {inner_str}")
353        }
354    }
355
356    /// Emit a real Lua if/else inside an IIFE so falsey then-arms stay correct.
357    fn generate_if_expr(
358        &self,
359        condition: &Expression,
360        then_expr: &Expression,
361        else_expr: &Expression,
362    ) -> String {
363        let condition = self.generate_expression(condition);
364        let then_expr = self.generate_expression(then_expr);
365        let else_expr = self.generate_expression(else_expr);
366        format!(
367            "(function() if {condition} then return {then_expr} else return {else_expr} end end)()"
368        )
369    }
370
371    fn generate_closure(&self, params: &[String], body: &factorio_ir::block::Block) -> String {
372        let params = params.join(", ");
373        // Single-statement `return expr` -> compact one-liner.
374        if let [factorio_ir::statement::Statement::Return(Some(expr))] = body.statements.as_slice()
375        {
376            let expr = self.generate_expression(expr);
377            return format!("function({params}) return {expr} end");
378        }
379
380        let mut temp = self.fork_expr_emitter();
381        let _ = writeln!(temp.output, "function({params})");
382        temp.indent_level = 1;
383        let _ = temp.generate_block(body, None);
384        temp.indent_level = 0;
385        temp.write_line("end");
386        // Drop the trailing newline so call-sites can append `)` cleanly.
387        while temp.output.ends_with('\n') {
388            temp.output.pop();
389        }
390        temp.output
391    }
392}
393
394fn is_storage_receiver(receiver: &Expression) -> bool {
395    match receiver {
396        Expression::Identifier(name) => name == "storage",
397        Expression::QualifiedPath { segments } => {
398            segments.last().is_some_and(|name| name == "storage")
399        }
400        _ => false,
401    }
402}
403
404/// Drop trailing `nil` literals from call/method argument lists.
405fn trim_trailing_nils(args: &[Expression]) -> &[Expression] {
406    let mut end = args.len();
407    while end > 0 {
408        match &args[end - 1] {
409            Expression::Literal(factorio_ir::literal::Literal::Nil) => end -= 1,
410            _ => break,
411        }
412    }
413    &args[..end]
414}