use std::fmt::Write as _;
use factorio_ir::{block::Block, literal::Literal, module::Module, scope::Scope};
use crate::generator::error::LuaGeneratorResult;
pub mod comment;
pub mod error;
pub mod expression;
pub mod function;
pub mod operator;
pub mod statement;
pub mod table;
pub struct LuaGenerator {
output: String,
indent_level: usize,
struct_table_context: Option<(String, String)>,
debug_level: Option<u8>,
mod_name: String,
for_depth: usize,
module_prefix: String,
profile: Option<String>,
}
impl Default for LuaGenerator {
fn default() -> Self {
Self::new()
}
}
impl LuaGenerator {
#[must_use]
pub fn new() -> Self {
Self::with_mod_name("mod")
}
#[must_use]
pub fn with_mod_name(mod_name: impl Into<String>) -> Self {
Self {
output: String::new(),
indent_level: 0,
struct_table_context: None,
debug_level: None,
mod_name: mod_name.into(),
for_depth: 0,
module_prefix: String::new(),
profile: None,
}
}
#[must_use]
pub fn with_debug_level(debug_level: u8) -> Self {
Self {
output: String::new(),
indent_level: 0,
struct_table_context: None,
debug_level: Some(debug_level),
mod_name: "mod".to_string(),
for_depth: 0,
module_prefix: String::new(),
profile: None,
}
}
#[must_use]
pub fn with_mod_name_and_debug(mod_name: impl Into<String>, debug_level: u8) -> Self {
Self {
output: String::new(),
indent_level: 0,
struct_table_context: None,
debug_level: Some(debug_level),
mod_name: mod_name.into(),
for_depth: 0,
module_prefix: String::new(),
profile: None,
}
}
#[must_use]
pub fn with_module_prefix(mut self, prefix: impl Into<String>) -> Self {
self.module_prefix = prefix.into();
self
}
#[must_use]
pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
self.profile = Some(profile.into());
self
}
const fn debug_level_at_least(&self, level: u8) -> bool {
matches!(self.debug_level, Some(current) if current >= level)
}
pub const INDENT: &'static str = "\t";
fn indent(&self) -> String {
Self::INDENT.repeat(self.indent_level)
}
fn write_line(&mut self, line: &str) {
if !line.is_empty() {
self.output.push_str(&self.indent());
self.output.push_str(line);
}
self.output.push('\n');
}
fn write_doc_comments(&mut self, doc: Option<&str>) {
let Some(doc) = doc else {
return;
};
for line in doc.lines() {
if line.is_empty() {
self.write_line("--");
} else {
self.write_line(&format!("-- {line}"));
}
}
}
pub const MODULE_META_START: &'static str =
concat!("-- Generated by factorio-rs@", env!("CARGO_PKG_VERSION"));
fn generate_module_meta(&self, module: &Module) -> String {
let mut header = format!("{}\n", Self::MODULE_META_START);
if let Some(profile) = &self.profile {
let _ = writeln!(header, "-- Profile: {profile}");
}
let _ = write!(header, "-- Module: `{}`", module.name);
header
}
fn module_identifier(module_name: &str) -> String {
heck::AsLowerCamelCase(module_name.replace('.', "_")).to_string()
}
fn mod_require_path(&self, module_path: &str) -> String {
let path = module_path.replace('.', "/");
let prefixed_path = self.apply_path_prefix(&path);
format!("__{}__/lua/{}", self.mod_name, prefixed_path)
}
#[must_use]
pub fn apply_path_prefix(&self, path: &str) -> String {
if self.module_prefix.is_empty() {
return path.to_string();
}
path.rfind('/').map_or_else(
|| format!("{}_{}", self.module_prefix, path),
|slash| {
format!(
"{}/{prefix}_{}",
&path[..slash],
&path[slash + 1..],
prefix = self.module_prefix
)
},
)
}
#[must_use]
pub fn prefixed_local(&self, local: &str) -> String {
if self.module_prefix.is_empty() {
local.to_string()
} else {
format!("{}_{}", self.module_prefix, local)
}
}
pub fn generate_module(&mut self, module: &Module) -> LuaGeneratorResult<String> {
self.output.clear();
self.indent_level = 0;
let module_header = self.generate_module_meta(module);
self.output.push_str(&module_header);
self.output.push('\n');
self.generate_imports(&module.imports);
for statement in &module.body.statements {
self.generate_statement(statement, Some(module), None, Scope::Private)?;
}
let module_name = Self::module_identifier(&module.name);
let (exporter_start, exporter_end) = Self::generate_symbol_exporter(&module_name);
self.write_line(&exporter_start);
if !module.submodules.is_empty() {
self.write_line(&format!(
"package.loaded[\"{}\"] = {module_name}",
self.mod_require_path(&module.name)
));
}
for symbol in &module.symbols {
self.generate_statement(
&symbol.statement,
Some(module),
Some(&module_name),
symbol.scope,
)?;
}
self.generate_submodules(&module.submodules);
self.write_line(&exporter_end);
Ok(self.output.clone())
}
fn generate_imports(&mut self, imports: &[factorio_ir::module::ModuleImport]) {
for import in imports {
self.write_line(&format!(
"local {} = require(\"{}\")",
import.local,
self.mod_require_path(&import.module)
));
for item in &import.items {
self.write_line(&format!(
"local {} = {}.{}",
item.local, import.local, item.name
));
}
}
if !imports.is_empty() {
self.output.push('\n');
}
}
fn generate_submodules(&mut self, submodules: &[String]) {
for submodule in submodules {
self.write_line(&format!(
"require(\"{}\")",
self.mod_require_path(submodule)
));
}
if !submodules.is_empty() {
self.output.push('\n');
}
}
fn generate_symbol_exporter(module_name: &str) -> (String, String) {
(
format!("local {module_name} = {{}}"),
format!("return {module_name}"),
)
}
fn generate_block(&mut self, block: &Block, module: Option<&Module>) -> LuaGeneratorResult<()> {
for statement in &block.statements {
self.generate_statement(statement, module, None, Scope::Private)?;
}
Ok(())
}
fn generate_literal(literal: &Literal) -> String {
match literal {
Literal::Int(value) => value.to_string(),
Literal::Float(value) => value.to_string(),
Literal::String(value) => format!("\"{}\"", escape_lua_string(value)),
Literal::Bool(value) => value.to_string(),
Literal::Nil => "nil".to_string(),
}
}
fn format_parameter(&self, parameter: &factorio_ir::function::Parameter) -> String {
let type_comment = self.parameter_type_comment(parameter.source_type.as_deref());
format!("{}{type_comment}", parameter.name)
}
}
fn escape_lua_string(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}