Skip to main content

factorio_codegen/generator/
expression.rs

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