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