factorio-frontend 0.1.0

Rust frontend that lowers Factorio mod sources into factorio-ir
Documentation
use syn::parse::{Parse, ParseStream};
use syn::{Expr, ExprMacro, LitStr};

use crate::error::{FrontendError, FrontendResult};

use super::{context::LowerContext, expressions::lower_expression, util::location};

struct PrintlnMacroInput {
    format: LitStr,
    args: Vec<Expr>,
}

impl Parse for PrintlnMacroInput {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let format = input.parse::<LitStr>()?;
        let mut args = Vec::new();

        while input.parse::<syn::Token![,]>().is_ok() {
            args.push(input.parse::<Expr>()?);
        }

        Ok(Self { format, args })
    }
}

pub fn lower_macro_expression(
    mac: &ExprMacro,
    ctx: &mut LowerContext<'_>,
    self_type: Option<&str>,
) -> FrontendResult<factorio_ir::expression::Expression> {
    let name = mac.mac.path.get_ident().map_or_else(
        || {
            mac.mac
                .path
                .segments
                .last()
                .map_or_else(|| "macro".to_string(), |segment| segment.ident.to_string())
        },
        ToString::to_string,
    );

    if name != "println" {
        return Err(FrontendError::UnsupportedMacro {
            name,
            location: location(mac),
        });
    }

    let input = syn::parse2::<PrintlnMacroInput>(mac.mac.tokens.clone())?;
    let template = input.format.value();
    let lowered_args = input
        .args
        .iter()
        .map(|arg| lower_expression(arg, ctx, self_type))
        .collect::<FrontendResult<Vec<_>>>()?;
    let message = lower_format_template(&template, &lowered_args, location(mac))?;

    Ok(factorio_ir::expression::Expression::Call {
        func: Box::new(factorio_ir::expression::Expression::FieldAccess {
            base: Box::new(factorio_ir::expression::Expression::Identifier(
                "game".to_string(),
            )),
            field: "print".to_string(),
        }),
        args: vec![message],
    })
}

fn lower_format_template(
    template: &str,
    args: &[factorio_ir::expression::Expression],
    location: String,
) -> FrontendResult<factorio_ir::expression::Expression> {
    let pieces = parse_format_pieces(template);
    let sequential_count = pieces
        .iter()
        .filter(|piece| matches!(piece, FormatPiece::PositionalArg))
        .count();

    if sequential_count > args.len() {
        return Err(FrontendError::FormatArgumentMismatch {
            template: template.to_string(),
            expected: sequential_count,
            found: args.len(),
            location,
        });
    }

    for piece in &pieces {
        if let FormatPiece::PositionalIndex(index) = piece
            && *index >= args.len()
        {
            return Err(FrontendError::FormatArgumentMismatch {
                template: template.to_string(),
                expected: *index + 1,
                found: args.len(),
                location,
            });
        }
    }

    let mut parts = Vec::new();
    let mut sequential_index = 0;

    for piece in pieces {
        match piece {
            FormatPiece::Literal(value) => {
                parts.push(factorio_ir::expression::Expression::Literal(
                    factorio_ir::literal::Literal::String(value),
                ));
            }
            FormatPiece::PositionalArg => {
                let Some(arg) = args.get(sequential_index).cloned() else {
                    return Err(FrontendError::FormatArgumentMismatch {
                        template: template.to_string(),
                        expected: sequential_index + 1,
                        found: args.len(),
                        location,
                    });
                };
                parts.push(arg);
                sequential_index += 1;
            }
            FormatPiece::PositionalIndex(index) => {
                parts.push(args[index].clone());
            }
            FormatPiece::NamedCapture(name) => {
                parts.push(factorio_ir::expression::Expression::Identifier(name));
            }
        }
    }

    if parts.is_empty() {
        return Ok(factorio_ir::expression::Expression::Literal(
            factorio_ir::literal::Literal::String(String::new()),
        ));
    }

    if parts.len() == 1 {
        return Ok(parts.remove(0));
    }

    Ok(factorio_ir::expression::Expression::FormatConcat { parts })
}

enum FormatPiece {
    Literal(String),
    PositionalArg,
    PositionalIndex(usize),
    NamedCapture(String),
}

fn parse_format_pieces(template: &str) -> Vec<FormatPiece> {
    let mut pieces = Vec::new();
    let mut literal = String::new();
    let mut chars = template.chars().peekable();

    while let Some(ch) = chars.next() {
        match ch {
            '{' => {
                if matches!(chars.peek(), Some('{')) {
                    chars.next();
                    literal.push('{');
                    continue;
                }

                if !literal.is_empty() {
                    pieces.push(FormatPiece::Literal(std::mem::take(&mut literal)));
                }

                let mut contents = String::new();
                let mut closed = false;
                for c in chars.by_ref() {
                    if c == '}' {
                        closed = true;
                        break;
                    }
                    contents.push(c);
                }

                if !closed {
                    literal.push('{');
                    literal.push_str(&contents);
                    continue;
                }

                pieces.push(parse_format_placeholder(&contents));
            }
            '}' => {
                if matches!(chars.peek(), Some('}')) {
                    chars.next();
                }
                literal.push('}');
            }
            other => literal.push(other),
        }
    }

    if !literal.is_empty() {
        pieces.push(FormatPiece::Literal(literal));
    }

    pieces
}

fn parse_format_placeholder(contents: &str) -> FormatPiece {
    if contents.is_empty() {
        return FormatPiece::PositionalArg;
    }

    let name = contents.split(':').next().unwrap_or(contents);

    if name.is_empty() {
        return FormatPiece::PositionalArg;
    }

    if let Ok(index) = name.parse::<usize>() {
        return FormatPiece::PositionalIndex(index);
    }

    if is_format_ident(name) {
        return FormatPiece::NamedCapture(name.to_string());
    }

    FormatPiece::PositionalArg
}

fn is_format_ident(name: &str) -> bool {
    let mut chars = name.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    (first == '_' || first.is_ascii_alphabetic())
        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}