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
34pub 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
47pub 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
66pub fn parse_discovered_module(
71 discovered: &crate::discovery::DiscoveredModule,
72) -> FrontendResult<factorio_ir::module::Module> {
73 parse_discovered_module_with_prefix(discovered, "")
74}
75
76pub 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 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
346fn 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}