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, prototype_lua_typename};
6
7impl LuaGenerator {
8    #[must_use]
9    pub fn generate_expression(&self, expression: &Expression) -> String {
10        self.generate_expression_prec(expression, 0)
11    }
12
13    pub(crate) fn generate_expression_prec(&self, expression: &Expression, min_prec: u8) -> String {
14        match expression {
15            Expression::BinaryOp { lhs, op, rhs } => {
16                // `0 - x` (or `0.0 - x`) is the frontend's encoding of unary negation.
17                // Emit as Lua `-x` directly.
18                let is_zero_lhs = match lhs.as_ref() {
19                    Expression::Literal(factorio_ir::literal::Literal::Int(0)) => true,
20                    Expression::Literal(factorio_ir::literal::Literal::Float(f)) => *f == 0.0,
21                    _ => false,
22                };
23                if matches!(op, factorio_ir::operator::Operator::Sub) && is_zero_lhs {
24                    let rhs_str = self.generate_expression_prec(rhs, 100);
25                    let result = format!("-{rhs_str}");
26                    return if 0 < min_prec {
27                        format!("({result})")
28                    } else {
29                        result
30                    };
31                }
32
33                let prec = Self::operator_precedence(*op);
34                let lhs_str = self.generate_expression_prec(lhs, prec);
35                let rhs_str = self.generate_expression_prec(rhs, prec.saturating_add(1));
36                let result = format!("{} {} {}", lhs_str, Self::generate_operator(*op), rhs_str);
37
38                if prec < min_prec {
39                    format!("({result})")
40                } else {
41                    result
42                }
43            }
44            _ => self.generate_atom(expression),
45        }
46    }
47
48    /// Generate the smallest level of code (an atom).
49    pub(crate) fn generate_atom(&self, expression: &Expression) -> String {
50        match expression {
51            Expression::Literal(literal) => Self::generate_literal(literal),
52            Expression::Identifier(name) => self.generate_identifier(name),
53            Expression::FieldAccess { base, field } => {
54                let base = self.generate_expression(base);
55                format!("{base}.{field}")
56            }
57            Expression::QualifiedPath { segments } => self.generate_qualified_path(segments),
58            Expression::Call { func, args } => self.generate_call(func, args),
59            Expression::MethodCall {
60                receiver,
61                method,
62                args,
63            } => self.generate_method_call(receiver, method, args),
64            Expression::StructLiteral {
65                struct_name,
66                fields,
67            } => self.generate_struct_literal(struct_name.as_deref(), fields),
68            Expression::EnumLiteral {
69                enum_name,
70                variant,
71                fields,
72            } => self.generate_enum_literal(enum_name, variant, fields),
73            Expression::FormatConcat { parts } => parts
74                .iter()
75                .map(|part| self.generate_expression(part))
76                .collect::<Vec<_>>()
77                .join(" .. "),
78            Expression::Array { elements } => {
79                let elements = elements
80                    .iter()
81                    .map(|element| self.generate_expression(element))
82                    .collect::<Vec<_>>()
83                    .join(", ");
84                format!("{{ {elements} }}")
85            }
86            Expression::Index { base, key } => self.generate_index(base, key),
87            Expression::Not(inner) => self.generate_not(inner),
88            Expression::Len(inner) => {
89                let inner = self.generate_expression(inner);
90                format!("#{inner}")
91            }
92            Expression::If {
93                condition,
94                then_expr,
95                else_expr,
96            } => self.generate_if_expr(condition, then_expr, else_expr),
97            Expression::Closure { params, body } => self.generate_closure(params, body),
98            Expression::FatPointer { data, vtable } => {
99                let data = self.generate_expression(data);
100                format!("{{ _data = {data}, _vt = {vtable} }}")
101            }
102            Expression::DynMethodCall {
103                receiver,
104                method,
105                args,
106            } => {
107                let recv = self.generate_expression(receiver);
108                let args_lua = self.generate_arg_list(args);
109                if args_lua.is_empty() {
110                    format!("{recv}._vt.{method}({recv})")
111                } else {
112                    format!("{recv}._vt.{method}({recv}, {args_lua})")
113                }
114            }
115            Expression::BinaryOp { .. } => {
116                unreachable!("binary operators are handled by generate_expression_prec")
117            }
118        }
119    }
120
121    /// Resolve a bare name: exported `pub fn`s become `module.name`, locals stay bare.
122    fn generate_identifier(&self, name: &str) -> String {
123        if self.exported_functions.contains(name)
124            && let Some(module_table) = &self.current_module_table
125        {
126            return format!("{module_table}.{name}");
127        }
128        name.to_string()
129    }
130
131    fn generate_qualified_path(&self, segments: &[String]) -> String {
132        if let Some((struct_name, table_path)) = &self.struct_table_context
133            && segments
134                .first()
135                .is_some_and(|segment| segment == struct_name)
136        {
137            let suffix = segments
138                .get(1..)
139                .map_or_else(String::new, |rest| rest.join("."));
140            if suffix.is_empty() {
141                return table_path.clone();
142            }
143            return format!("{table_path}.{suffix}");
144        }
145
146        segments.join(".")
147    }
148
149    fn generate_call(&self, func: &Expression, args: &[Expression]) -> String {
150        if let Expression::QualifiedPath { segments } = func
151            && args.is_empty()
152            && segments
153                .last()
154                .is_some_and(|s| s == "new" || s == "default")
155        {
156            match segments[0].as_str() {
157                "LuaAny" => return "nil".to_string(),
158                "Vec" if segments.last().is_some_and(|s| s == "new") => {
159                    return "{}".to_string();
160                }
161                _ if segments.last().is_some_and(|s| s == "default") => {
162                    return "{}".to_string();
163                }
164                _ => {}
165            }
166        }
167
168        let func_is_closure = matches!(func, Expression::Closure { .. });
169        let func = self.generate_expression(func);
170        let args = self.generate_arg_list(args);
171        if func_is_closure {
172            format!("({func})({args})")
173        } else {
174            format!("{func}({args})")
175        }
176    }
177
178    fn generate_method_call(
179        &self,
180        receiver: &Expression,
181        method: &str,
182        args: &[Expression],
183    ) -> String {
184        // `storage.get(key)` -> `storage[key]` (missing -> nil / Option::None).
185        // Must run before the settings `.get` rewrite (`recv[key].value`).
186        if method == "get" && args.len() == 1 && is_storage_receiver(receiver) {
187            let receiver = self.generate_expression(receiver);
188            let key = self.generate_expression(&args[0]);
189            return format!("{receiver}[{key}]");
190        }
191
192        if method == "get" && args.len() == 1 {
193            let receiver = self.generate_expression(receiver);
194            let key = self.generate_expression(&args[0]);
195            return format!("{receiver}[{key}].value");
196        }
197
198        // Typed settings accessors share the same Lua shape as `.get`.
199        if matches!(
200            method,
201            "get_bool" | "get_int" | "get_double" | "get_string" | "setting"
202        ) && args.len() == 1
203        {
204            let receiver = self.generate_expression(receiver);
205            let key = self.generate_expression(&args[0]);
206            if method == "setting" {
207                return format!("{receiver}[{key}]");
208            }
209            return format!("{receiver}[{key}].value");
210        }
211
212        // `storage.set(key, value)` -> `storage[key] = value` (Factorio persistent table).
213        if method == "set" && args.len() == 2 && is_storage_receiver(receiver) {
214            let receiver = self.generate_expression(receiver);
215            let key = self.generate_expression(&args[0]);
216            let value = self.generate_expression(&args[1]);
217            return format!("{receiver}[{key}] = {value}");
218        }
219
220        if method == "len" && args.is_empty() {
221            let receiver = self.generate_expression(receiver);
222            return format!("#{receiver}");
223        }
224
225        if method == "push" && args.len() == 1 {
226            let receiver = self.generate_expression(receiver);
227            let item = self.generate_expression(&args[0]);
228            return format!("table.insert({receiver}, {item})");
229        }
230
231        if method == "is_empty" && args.is_empty() {
232            let receiver = self.generate_expression(receiver);
233            return format!("#{receiver} == 0");
234        }
235
236        let trimmed = trim_trailing_nils(args);
237        if trimmed.is_empty() {
238            let receiver = self.generate_expression(receiver);
239            // Zero-arg API attributes are property reads (`entity.surface`).
240            // Calls that only passed trailing `None`s stay invocations (`entity.die()`).
241            if args.is_empty() {
242                return format!("{receiver}.{method}");
243            }
244            return format!("{receiver}.{method}()");
245        }
246
247        // Attribute writers (`set_caption` / `write_driving`) -> property assign.
248        // Real Factorio `set_*` methods and user methods are absent from the lookup.
249        if trimmed.len() == 1
250            && let Some(property) = attribute_property_for_setter(method)
251        {
252            let receiver = self.generate_expression(receiver);
253            let value = self.generate_expression(&trimmed[0]);
254            return format!("{receiver}.{property} = {value}");
255        }
256
257        let receiver = self.generate_expression(receiver);
258        let args_lua = self.generate_arg_list(trimmed);
259        format!("{receiver}.{method}({args_lua})")
260    }
261
262    /// Join call arguments, omitting trailing `nil` so Factorio optional params stay unset.
263    fn generate_arg_list(&self, args: &[Expression]) -> String {
264        trim_trailing_nils(args)
265            .iter()
266            .map(|arg| self.generate_expression(arg))
267            .collect::<Vec<_>>()
268            .join(", ")
269    }
270
271    fn generate_struct_literal(
272        &self,
273        struct_name: Option<&str>,
274        fields: &[(String, Expression)],
275    ) -> String {
276        if let Some(literal) = self.try_special_struct_literal(struct_name, fields) {
277            return self.maybe_struct_metatable(literal);
278        }
279
280        // Recipe ingredients: `type = "item"` or `"fluid"` from the `fluid` bool field.
281        let (injected_type, skip_fields): (Option<&str>, &[&str]) =
282            if struct_name == Some("RecipeIngredient") {
283                let is_fluid = fields.iter().any(|(n, v)| {
284                    n == "fluid"
285                        && matches!(
286                            v,
287                            Expression::Literal(factorio_ir::literal::Literal::Bool(true))
288                        )
289                });
290                (Some(if is_fluid { "fluid" } else { "item" }), &["fluid"])
291            } else {
292                (struct_name.and_then(prototype_lua_typename), &[])
293            };
294
295        let type_prefix = injected_type.map(|t| format!("type = \"{t}\", "));
296        let field_strs = fields
297            .iter()
298            .filter(|(name, value)| {
299                if skip_fields.contains(&name.as_str()) {
300                    return false;
301                }
302                if matches!(
303                    value,
304                    Expression::Literal(factorio_ir::literal::Literal::Nil)
305                ) {
306                    return false;
307                }
308                // Omit false optional flags that only affect type injection.
309                if name == "fluid"
310                    && matches!(
311                        value,
312                        Expression::Literal(factorio_ir::literal::Literal::Bool(false))
313                    )
314                {
315                    return false;
316                }
317                injected_type.is_none() || (name != "type" && name != "r#type")
318            })
319            .map(|(name, value)| {
320                let lua_name = if name == "r#type" {
321                    "type"
322                } else {
323                    name.as_str()
324                };
325                format!("{lua_name} = {}", self.generate_expression(value))
326            })
327            .collect::<Vec<_>>()
328            .join(", ");
329
330        let inner = match type_prefix {
331            Some(prefix) if !field_strs.is_empty() => format!("{prefix}{field_strs}"),
332            Some(prefix) => prefix.trim_end_matches(", ").to_string(),
333            None => field_strs,
334        };
335        self.maybe_struct_metatable(format!("{{ {inner} }}"))
336    }
337
338    /// Special Factorio shapes (tech ingredients, flag sets, `Tags`, `BoundingBox`).
339    fn try_special_struct_literal(
340        &self,
341        struct_name: Option<&str>,
342        fields: &[(String, Expression)],
343    ) -> Option<String> {
344        // Research ingredients are Factorio tuples `{ "pack", amount }`, not named tables.
345        if struct_name == Some("TechnologyUnitIngredient") {
346            let name = fields
347                .iter()
348                .find_map(|(n, v)| (n == "name").then(|| self.generate_expression(v)))?;
349            let amount = fields
350                .iter()
351                .find_map(|(n, v)| (n == "amount").then(|| self.generate_expression(v)))?;
352            return Some(format!("{{ {name}, {amount} }}"));
353        }
354
355        // Flag sets: `{ flags = {"left", "right"} }` -> `{ ["left"] = true, ... }`.
356        if let Some(name) = struct_name
357            && is_flag_set_struct(name)
358            && let Some(flags_expr) = fields.iter().find_map(|(n, v)| (n == "flags").then_some(v))
359        {
360            return Some(generate_flag_set_table(flags_expr));
361        }
362
363        // Tags / PropertyExpressionNames: `{ pairs = [...] }` -> `{ [key] = value, ... }`.
364        if matches!(struct_name, Some("Tags" | "PropertyExpressionNames"))
365            && let Some(pairs_expr) = fields.iter().find_map(|(n, v)| (n == "pairs").then_some(v))
366        {
367            return Some(self.generate_string_pair_table(pairs_expr));
368        }
369
370        // Bounding box: Factorio expects `{{left_top}, {right_bottom}}`.
371        if struct_name == Some("BoundingBox") {
372            let get = |key: &str| {
373                fields
374                    .iter()
375                    .find_map(|(n, v)| (n == key).then(|| self.generate_expression(v)))
376            };
377            if let (Some(lx), Some(ly), Some(rx), Some(ry)) = (
378                get("left_top_x"),
379                get("left_top_y"),
380                get("right_bottom_x"),
381                get("right_bottom_y"),
382            ) {
383                return Some(format!("{{{{ {lx}, {ly} }}, {{ {rx}, {ry} }}}}"));
384            }
385        }
386
387        None
388    }
389
390    fn maybe_struct_metatable(&self, literal: String) -> String {
391        if let Some((_, table_path)) = &self.struct_table_context {
392            format!("setmetatable({literal}, {{ __index = {table_path} }})")
393        } else {
394            literal
395        }
396    }
397
398    /// Array of `{ key, value }` structs -> `{ [key] = value, ... }`.
399    fn generate_string_pair_table(&self, pairs_expr: &Expression) -> String {
400        let entries = match pairs_expr {
401            Expression::Array { elements } => elements
402                .iter()
403                .filter_map(|item| match item {
404                    Expression::StructLiteral { fields, .. } => {
405                        let key = fields
406                            .iter()
407                            .find_map(|(n, v)| (n == "key").then(|| self.generate_expression(v)))?;
408                        let value = fields.iter().find_map(|(n, v)| {
409                            (n == "value").then(|| self.generate_expression(v))
410                        })?;
411                        Some(format!("[{key}] = {value}"))
412                    }
413                    _ => None,
414                })
415                .collect::<Vec<_>>(),
416            _ => Vec::new(),
417        };
418        format!("{{ {} }}", entries.join(", "))
419    }
420
421    fn generate_enum_literal(
422        &self,
423        enum_name: &str,
424        variant: &str,
425        fields: &[(String, Expression)],
426    ) -> String {
427        let mut parts = vec![format!("tag = \"{variant}\"")];
428        parts.extend(
429            fields
430                .iter()
431                .map(|(name, value)| format!("{name} = {}", self.generate_expression(value))),
432        );
433        let literal = format!("{{ {} }}", parts.join(", "));
434        if let Some((name, table_path)) = &self.struct_table_context
435            && name == enum_name
436        {
437            format!("setmetatable({literal}, {{ __index = {table_path} }})")
438        } else {
439            literal
440        }
441    }
442
443    fn generate_index(&self, base: &Expression, key: &Expression) -> String {
444        let base = self.generate_expression(base);
445
446        // Lua is 1-indexed: shift Rust integer literals (`0` -> `1`, `1` -> `2`, ...).
447        // Variable indices are left as-is (callers should use 1-based values).
448        let key = match key {
449            Expression::Literal(factorio_ir::literal::Literal::Int(index)) => {
450                (*index + 1).to_string()
451            }
452            _ => self.generate_expression(key),
453        };
454        format!("{base}[{key}]")
455    }
456
457    fn generate_not(&self, inner: &Expression) -> String {
458        if let Expression::MethodCall {
459            receiver,
460            method,
461            args,
462        } = inner
463            && method == "is_empty"
464            && args.is_empty()
465        {
466            let receiver = self.generate_expression(receiver);
467            return format!("#{receiver} ~= 0");
468        }
469
470        let needs_parens = matches!(inner, Expression::BinaryOp { .. });
471        let inner_str = self.generate_expression(inner);
472        if needs_parens {
473            format!("not ({inner_str})")
474        } else {
475            format!("not {inner_str}")
476        }
477    }
478
479    /// Emit a real Lua if/else inside an IIFE so falsey then-arms stay correct.
480    fn generate_if_expr(
481        &self,
482        condition: &Expression,
483        then_expr: &Expression,
484        else_expr: &Expression,
485    ) -> String {
486        let condition = self.generate_expression(condition);
487        let then_expr = self.generate_expression(then_expr);
488        let else_expr = self.generate_expression(else_expr);
489        format!(
490            "(function() if {condition} then return {then_expr} else return {else_expr} end end)()"
491        )
492    }
493
494    fn generate_closure(&self, params: &[String], body: &factorio_ir::block::Block) -> String {
495        let params = params.join(", ");
496        // Single-statement `return expr` -> compact one-liner.
497        if let [factorio_ir::statement::Statement::Return(Some(expr))] = body.statements.as_slice()
498        {
499            let expr = self.generate_expression(expr);
500            return format!("function({params}) return {expr} end");
501        }
502
503        let mut temp = self.fork_expr_emitter();
504        let _ = writeln!(temp.output, "function({params})");
505        temp.indent_level = 1;
506        let _ = temp.generate_block(body, None);
507        temp.indent_level = 0;
508        temp.write_line("end");
509        // Drop the trailing newline so call-sites can append `)` cleanly.
510        while temp.output.ends_with('\n') {
511            temp.output.pop();
512        }
513        temp.output
514    }
515}
516
517fn is_storage_receiver(receiver: &Expression) -> bool {
518    match receiver {
519        Expression::Identifier(name) => name == "storage",
520        Expression::QualifiedPath { segments } => {
521            segments.last().is_some_and(|name| name == "storage")
522        }
523        _ => false,
524    }
525}
526
527fn is_flag_set_struct(name: &str) -> bool {
528    matches!(
529        name,
530        "MouseButtonFlags"
531            | "SelectionModeFlags"
532            | "EntityPrototypeFlags"
533            | "ItemPrototypeFlags"
534            | "TriggerTargetMask"
535    )
536}
537
538/// `{ "left", "right" }` array -> `{ ["left"] = true, ... }`.
539fn generate_flag_set_table(flags_expr: &Expression) -> String {
540    let keys = match flags_expr {
541        Expression::Array { elements } => elements
542            .iter()
543            .filter_map(|item| match item {
544                Expression::Literal(factorio_ir::literal::Literal::String(s)) => {
545                    Some(format!("[\"{s}\"] = true"))
546                }
547                _ => None,
548            })
549            .collect::<Vec<_>>(),
550        _ => Vec::new(),
551    };
552    format!("{{ {} }}", keys.join(", "))
553}
554
555/// Drop trailing `nil` literals from call/method argument lists.
556fn trim_trailing_nils(args: &[Expression]) -> &[Expression] {
557    let mut end = args.len();
558    while end > 0 {
559        match &args[end - 1] {
560            Expression::Literal(factorio_ir::literal::Literal::Nil) => end -= 1,
561            _ => break,
562        }
563    }
564    &args[..end]
565}