Skip to main content

factorio_frontend/lower/
mod.rs

1use std::collections::BTreeMap;
2
3use syn::{ImplItem, Item, Visibility};
4
5use crate::{
6    error::{FrontendError, FrontendResult},
7    paths::require_local_name,
8};
9
10pub mod attrs;
11pub mod context;
12pub mod event_filter;
13pub mod event_handler;
14pub mod expressions;
15pub mod functions;
16pub mod imports;
17mod locale;
18pub mod metadata;
19mod mod_settings;
20pub mod print;
21pub mod statements;
22pub mod structs;
23pub mod types;
24pub mod util;
25
26use context::LowerContext;
27use expressions::lower_expression;
28use functions::{lower_function, lower_impl_method};
29use imports::{lower_use, merge_imports};
30use metadata::{extract_doc_comments, struct_header_comment};
31use structs::{PendingStruct, impl_type_name, lower_struct_fields};
32use util::{item_name, item_name_impl, location};
33
34/// Parse Rust source into a [`factorio_ir::module::Module`].
35///
36/// `module_name` is used as the module identifier in the resulting IR.
37///
38/// # Errors
39/// Returns `Err` if the Rust source fails to parse
40pub fn parse_module(
41    source: &str,
42    module_name: &str,
43) -> FrontendResult<factorio_ir::module::Module> {
44    parse_module_with_prefix(source, module_name, "")
45}
46
47/// Like [`parse_module`] but applies `module_prefix` to all generated module
48/// local names (e.g. `"ms"` turns `settings` into `ms_settings`).
49///
50/// # Errors
51/// Returns `Err` if the Rust source fails to parse
52pub fn parse_module_with_prefix(
53    source: &str,
54    module_name: &str,
55    module_prefix: &str,
56) -> FrontendResult<factorio_ir::module::Module> {
57    let file = syn::parse_file(source)?;
58    let stage = factorio_ir::stage::Stage::from_module_name(module_name).ok_or_else(|| {
59        FrontendError::InvalidModuleStage {
60            module: module_name.to_string(),
61        }
62    })?;
63    lower_items(&file.items, module_name, stage, module_prefix)
64}
65
66/// Lower a discovered module into IR.
67///
68/// # Errors
69/// Returns `Err` if lowering fails.
70pub fn parse_discovered_module(
71    discovered: &crate::discovery::DiscoveredModule,
72) -> FrontendResult<factorio_ir::module::Module> {
73    parse_discovered_module_with_prefix(discovered, "")
74}
75
76/// Like [`parse_discovered_module`] but applies `module_prefix` to all
77/// generated module local names.
78///
79/// # Errors
80/// Returns `Err` if lowering fails.
81pub fn parse_discovered_module_with_prefix(
82    discovered: &crate::discovery::DiscoveredModule,
83    module_prefix: &str,
84) -> FrontendResult<factorio_ir::module::Module> {
85    lower_items(
86        &discovered.items,
87        &discovered.module_name,
88        discovered.stage,
89        module_prefix,
90    )
91}
92
93fn lower_items(
94    items: &[Item],
95    module_name: &str,
96    stage: factorio_ir::stage::Stage,
97    module_prefix: &str,
98) -> FrontendResult<factorio_ir::module::Module> {
99    let mut body = Vec::new();
100    let mut symbols = Vec::new();
101    let mut use_imports = Vec::new();
102    let mut inline_imports = Vec::new();
103    let mut submodules = Vec::new();
104    let mut structs = BTreeMap::<String, PendingStruct>::new();
105    let mut pending_locales = Vec::new();
106    let mut ctx = LowerContext {
107        imports: &mut inline_imports,
108        module_prefix,
109        bare_import_renames: std::collections::HashMap::new(),
110    };
111    let mut module_state = ModuleLowerState {
112        module_name,
113        stage,
114        body: &mut body,
115        symbols: &mut symbols,
116        use_imports: &mut use_imports,
117        submodules: &mut submodules,
118        structs: &mut structs,
119        pending_locales: &mut pending_locales,
120    };
121
122    for item in items {
123        lower_top_level_item(item, module_name, &mut module_state, &mut ctx)?;
124    }
125
126    finalize_pending_structs(structs, &mut body, &mut symbols);
127
128    let const_strings = locale::collect_const_strings(&body, &symbols);
129    let mut locales = Vec::new();
130    for tokens in pending_locales {
131        locales.extend(locale::expand(tokens, &const_strings)?);
132    }
133
134    let mut all_imports = use_imports;
135    all_imports.extend(inline_imports);
136
137    Ok(factorio_ir::module::Module {
138        name: module_name.to_string(),
139        stage,
140        body: factorio_ir::block::Block { statements: body },
141        symbols,
142        imports: merge_imports(all_imports, module_prefix),
143        submodules,
144        locales,
145    })
146}
147
148struct ModuleLowerState<'a> {
149    module_name: &'a str,
150    stage: factorio_ir::stage::Stage,
151    body: &'a mut Vec<factorio_ir::statement::Statement>,
152    symbols: &'a mut Vec<factorio_ir::module::Symbol>,
153    use_imports: &'a mut Vec<imports::ImportFragment>,
154    submodules: &'a mut Vec<String>,
155    structs: &'a mut BTreeMap<String, PendingStruct>,
156    pending_locales: &'a mut Vec<proc_macro2::TokenStream>,
157}
158
159fn lower_top_level_item(
160    item: &Item,
161    module_name: &str,
162    module_state: &mut ModuleLowerState<'_>,
163    ctx: &mut LowerContext<'_>,
164) -> FrontendResult<()> {
165    match item {
166        Item::Fn(function) => {
167            let lowered =
168                factorio_ir::statement::Statement::FunctionDecl(lower_function(function, ctx)?);
169            if let factorio_ir::statement::Statement::FunctionDecl(ref func) = lowered
170                && func.event.is_some()
171                && module_state.stage != factorio_ir::stage::Stage::Control
172            {
173                return Err(FrontendError::EventOutsideControlStage {
174                    module: module_state.module_name.to_string(),
175                });
176            }
177            push_scoped_statement(
178                lowered,
179                &function.vis,
180                module_state.body,
181                module_state.symbols,
182            );
183        }
184        Item::Const(item_const) => {
185            let value = lower_expression(&item_const.expr, ctx, None)?;
186            let name = item_const.ident.to_string();
187            push_scoped_statement(
188                factorio_ir::statement::Statement::VariableDecl {
189                    name,
190                    ty: factorio_ir::r#type::Type::Void,
191                    source_type: None,
192                    value,
193                },
194                &item_const.vis,
195                module_state.body,
196                module_state.symbols,
197            );
198        }
199        Item::Struct(item_struct) => lower_struct_item(item_struct, module_state.structs)?,
200        Item::Impl(item_impl) => lower_impl_item(item_impl, module_state.structs, ctx)?,
201        Item::Use(use_item) => {
202            let fragments = lower_use(use_item, ctx.module_prefix)?;
203            // Populate the bare-import rename map so that path expressions like
204            // `adjacent_blacklist::check` get rewritten to `ms_adjacent_blacklist.check`.
205            // We only do this for bare module imports (item == None), NOT for item
206            // imports like `use crate::settings::Settings` - that keeps the Factorio
207            // global `settings` safe from being renamed.
208            if !ctx.module_prefix.is_empty() {
209                for fragment in &fragments {
210                    if fragment.item.is_none() {
211                        let bare = require_local_name(&fragment.module);
212                        if bare != fragment.require_local {
213                            ctx.bare_import_renames
214                                .insert(bare, fragment.require_local.clone());
215                        }
216                    }
217                }
218            }
219            module_state.use_imports.extend(fragments);
220        }
221        Item::Mod(item_mod) if item_mod.content.is_none() => {
222            module_state
223                .submodules
224                .push(submodule_path(module_name, &item_mod.ident.to_string()));
225        }
226        Item::Mod(_) => {}
227        Item::Macro(mac) if is_known_macro(&mac.mac, "mod_settings") => {
228            let expanded = mod_settings::expand(mac.mac.tokens.clone())?;
229            for item in &expanded {
230                lower_top_level_item(item, module_name, module_state, ctx)?;
231            }
232        }
233        Item::Macro(mac) if is_known_macro(&mac.mac, "locale") => {
234            module_state.pending_locales.push(mac.mac.tokens.clone());
235        }
236        item => {
237            return Err(FrontendError::UnsupportedItem {
238                item: item_name(item),
239                location: location(item),
240            });
241        }
242    }
243
244    Ok(())
245}
246
247fn lower_struct_item(
248    item_struct: &syn::ItemStruct,
249    structs: &mut BTreeMap<String, PendingStruct>,
250) -> FrontendResult<()> {
251    let name = item_struct.ident.to_string();
252    let entry = structs
253        .entry(name)
254        .or_insert_with(|| PendingStruct::new(item_struct.vis.clone()));
255    entry.visibility = item_struct.vis.clone();
256    entry.fields = lower_struct_fields(&item_struct.fields)?;
257    entry.doc = extract_doc_comments(&item_struct.attrs);
258    Ok(())
259}
260
261fn lower_impl_item(
262    item_impl: &syn::ItemImpl,
263    structs: &mut BTreeMap<String, PendingStruct>,
264    ctx: &mut LowerContext<'_>,
265) -> FrontendResult<()> {
266    if item_impl.trait_.is_some() {
267        return Err(FrontendError::UnsupportedItem {
268            item: "trait impl".to_string(),
269            location: location(item_impl),
270        });
271    }
272
273    let struct_name = impl_type_name(&item_impl.self_ty)?;
274    let entry = structs
275        .entry(struct_name.clone())
276        .or_insert_with(|| PendingStruct::new(Visibility::Inherited));
277
278    for impl_item in &item_impl.items {
279        match impl_item {
280            ImplItem::Fn(method) => {
281                entry
282                    .methods
283                    .push(lower_impl_method(method, &struct_name, ctx)?);
284            }
285            ImplItem::Const(item) => {
286                let value = lower_expression(&item.expr, ctx, Some(&struct_name))?;
287                entry.constants.push((item.ident.to_string(), value));
288            }
289            item => {
290                return Err(FrontendError::UnsupportedItem {
291                    item: item_name_impl(item),
292                    location: location(item),
293                });
294            }
295        }
296    }
297
298    Ok(())
299}
300
301fn finalize_pending_structs(
302    structs: BTreeMap<String, PendingStruct>,
303    body: &mut Vec<factorio_ir::statement::Statement>,
304    symbols: &mut Vec<factorio_ir::module::Symbol>,
305) {
306    for (name, pending_struct) in structs {
307        let lowered =
308            factorio_ir::statement::Statement::StructDecl(factorio_ir::structure::Struct {
309                name: name.clone(),
310                fields: pending_struct.fields.clone(),
311                constants: pending_struct.constants,
312                methods: pending_struct.methods,
313                doc: pending_struct.doc,
314                debug: Some(factorio_ir::debug::StructDebug {
315                    header_comment: struct_header_comment(
316                        &pending_struct.visibility,
317                        &name,
318                        &pending_struct.fields,
319                    ),
320                }),
321            });
322
323        push_scoped_statement(lowered, &pending_struct.visibility, body, symbols);
324    }
325}
326
327fn push_scoped_statement(
328    statement: factorio_ir::statement::Statement,
329    visibility: &Visibility,
330    body: &mut Vec<factorio_ir::statement::Statement>,
331    symbols: &mut Vec<factorio_ir::module::Symbol>,
332) {
333    match visibility {
334        Visibility::Public(_) => symbols.push(factorio_ir::module::Symbol {
335            scope: factorio_ir::scope::Scope::Public,
336            statement,
337        }),
338        _ => body.push(statement),
339    }
340}
341
342fn submodule_path(module_name: &str, child: &str) -> String {
343    format!("{module_name}.{child}")
344}
345
346/// Returns `true` if the macro's path ends with `name` (e.g. `mod_settings`).
347/// Matches both `mod_settings!` and `factorio_rs::mod_settings!`.
348fn is_known_macro(mac: &syn::Macro, name: &str) -> bool {
349    mac.path
350        .segments
351        .last()
352        .is_some_and(|seg| seg.ident == name)
353}