factorio-frontend 0.1.3

Rust frontend that lowers Factorio mod sources into factorio-ir
Documentation
mod common;

use common::must_ok_parse;
use factorio_frontend::parse_module;
use factorio_ir::{
    expression::Expression, literal::Literal, operator::Operator, statement::Statement,
};

#[test]
fn lowers_ok_err_constructors() {
    let source = r#"
pub fn ok_one() -> Result<i32, String> {
    Ok(1)
}

pub fn err_msg() -> Result<i32, String> {
    Err("no")
}
"#;
    let module = must_ok_parse(parse_module(source, "control.result_ctors"));
    let ok_fn = module
        .symbols
        .iter()
        .find_map(|s| match &s.statement {
            Statement::FunctionDecl(f) if f.name == "ok_one" => Some(f),
            _ => None,
        })
        .expect("ok_one");
    let Statement::Return(Some(Expression::StructLiteral { fields, struct_name })) =
        &ok_fn.body.statements[0]
    else {
        panic!("expected Ok struct, got {:?}", ok_fn.body.statements);
    };
    assert_eq!(struct_name.as_deref(), Some("Result"));
    assert_eq!(fields, &vec![("ok".to_string(), Expression::Literal(Literal::Int(1)))]);

    let err_fn = module
        .symbols
        .iter()
        .find_map(|s| match &s.statement {
            Statement::FunctionDecl(f) if f.name == "err_msg" => Some(f),
            _ => None,
        })
        .expect("err_msg");
    let Statement::Return(Some(Expression::StructLiteral { fields, .. })) =
        &err_fn.body.statements[0]
    else {
        panic!("expected Err struct");
    };
    assert_eq!(fields[0].0, "err");
    assert_eq!(
        fields[0].1,
        Expression::Literal(Literal::String("no".to_string()))
    );
}

#[test]
fn lowers_try_operator() {
    let source = r#"
pub fn parse(_: &str) -> Result<i32, String> {
    Ok(1)
}

pub fn load(name: &str) -> Result<i32, String> {
    let n = parse(name)?;
    Ok(n + 1)
}
"#;
    let module = must_ok_parse(parse_module(source, "control.result_try"));
    let function = module
        .symbols
        .iter()
        .find_map(|s| match &s.statement {
            Statement::FunctionDecl(f) if f.name == "load" => Some(f),
            _ => None,
        })
        .expect("load");

    // local __try_N = parse(name)
    // if __try_N.err ~= nil then return __try_N end
    // local n = __try_N.ok
    // return { ok = n + 1 }
    assert!(function.body.statements.len() >= 4);
    let Statement::VariableDecl { name: try_name, .. } = &function.body.statements[0] else {
        panic!("expected try temp, got {:?}", function.body.statements[0]);
    };
    assert!(try_name.starts_with("__try_"));
    let Statement::Conditional {
        condition,
        then_block,
        ..
    } = &function.body.statements[1]
    else {
        panic!("expected err check");
    };
    assert_eq!(
        condition,
        &Expression::BinaryOp {
            lhs: Box::new(Expression::FieldAccess {
                base: Box::new(Expression::Identifier(try_name.clone())),
                field: "err".to_string(),
            }),
            op: Operator::Ne,
            rhs: Box::new(Expression::Literal(Literal::Nil)),
        }
    );
    assert!(matches!(
        then_block.as_slice(),
        [Statement::Return(Some(Expression::Identifier(n)))] if n == try_name
    ));
    let Statement::VariableDecl {
        name: n_name,
        value,
        ..
    } = &function.body.statements[2]
    else {
        panic!("expected n binding");
    };
    assert_eq!(n_name, "n");
    assert_eq!(
        value,
        &Expression::FieldAccess {
            base: Box::new(Expression::Identifier(try_name.clone())),
            field: "ok".to_string(),
        }
    );
}

#[test]
fn lowers_if_let_ok() {
    let source = r#"
pub fn handle(r: Result<i32, String>) {
    if let Ok(n) = r {
        return n;
    }
}
"#;
    let module = must_ok_parse(parse_module(source, "control.result_if_let"));
    let Statement::FunctionDecl(function) = &module.symbols[0].statement else {
        panic!("expected function");
    };
    assert!(matches!(
        function.body.statements.as_slice(),
        [
            Statement::VariableDecl { .. },
            Statement::Conditional {
                condition: Expression::BinaryOp {
                    op: Operator::Eq,
                    ..
                },
                then_block,
                ..
            }
        ] if matches!(
            then_block.as_slice(),
            [
                Statement::VariableDecl { name, .. },
                Statement::Return(Some(Expression::Identifier(ret)))
            ] if name == "n" && ret == "n"
        )
    ));
}

#[test]
fn lowers_match_ok_err() {
    let source = r#"
pub fn unwrap_or_zero(r: Result<i32, String>) -> i32 {
    match r {
        Ok(n) => n,
        Err(_) => 0,
    }
}
"#;
    let module = must_ok_parse(parse_module(source, "control.result_match"));
    let Statement::FunctionDecl(function) = &module.symbols[0].statement else {
        panic!("expected function");
    };
    let Statement::Return(Some(Expression::Call { func, .. })) = &function.body.statements[0]
    else {
        panic!("expected IIFE");
    };
    let Expression::Closure { body, .. } = func.as_ref() else {
        panic!("expected closure");
    };
    let Statement::Conditional {
        condition,
        then_block,
        ..
    } = &body.statements[1]
    else {
        panic!("expected Ok arm");
    };
    assert!(matches!(
        condition,
        Expression::BinaryOp {
            op: Operator::Eq,
            lhs,
            ..
        } if matches!(
            lhs.as_ref(),
            Expression::FieldAccess { field, .. } if field == "err"
        )
    ));
    assert!(matches!(
        then_block.as_slice(),
        [
            Statement::VariableDecl { name, .. },
            Statement::Return(Some(Expression::Identifier(ret)))
        ] if name == "n" && ret == "n"
    ));
}

#[test]
fn lowers_result_is_ok_map() {
    let source = r#"
pub fn bump(r: Result<i32, String>) -> Result<i32, String> {
    if r.is_ok() {
        return r.map(|n| n + 1);
    }
    r
}
"#;
    let module = must_ok_parse(parse_module(source, "control.result_methods"));
    let Statement::FunctionDecl(function) = &module.symbols[0].statement else {
        panic!("expected function");
    };
    let Statement::Conditional { condition, .. } = &function.body.statements[0] else {
        panic!("expected if");
    };
    assert!(matches!(
        condition,
        Expression::BinaryOp {
            op: Operator::Eq,
            lhs,
            ..
        } if matches!(
            lhs.as_ref(),
            Expression::FieldAccess { field, .. } if field == "err"
        )
    ));
}

#[test]
fn lowers_option_ok_or() {
    let source = r#"
pub fn place(entity: Option<i32>) -> Result<i32, String> {
    entity.ok_or("missing")
}
"#;
    let module = must_ok_parse(parse_module(source, "control.option_ok_or"));
    let Statement::FunctionDecl(function) = &module.symbols[0].statement else {
        panic!("expected function");
    };
    // Trivial receiver stays an if-expr (no bind IIFE).
    let Statement::Return(Some(Expression::If {
        condition,
        then_expr,
        else_expr,
    })) = &function.body.statements[0]
    else {
        panic!("expected if-expr return, got {:?}", function.body.statements);
    };
    assert!(matches!(
        condition.as_ref(),
        Expression::BinaryOp {
            op: Operator::Ne,
            ..
        }
    ));
    assert!(matches!(
        then_expr.as_ref(),
        Expression::StructLiteral { fields, .. } if fields[0].0 == "ok"
    ));
    assert!(matches!(
        else_expr.as_ref(),
        Expression::StructLiteral { fields, .. } if fields[0].0 == "err"
    ));
}

#[test]
fn ok_or_binds_side_effecting_receiver_once() {
    use factorio_codegen::LuaGenerator;

    let source = r#"
pub fn try_place(surface: LuaSurface, params: i32) -> Result<i32, String> {
    surface.create_entity(params).ok_or("failed")
}
"#;
    let module = must_ok_parse(parse_module(source, "control.ok_or_once"));
    let lua = LuaGenerator::new()
        .generate_module(&module)
        .expect("generate");
    let count = lua.matches("create_entity").count();
    assert_eq!(
        count, 1,
        "create_entity should be evaluated once, lua was:\n{lua}"
    );
    assert!(
        lua.contains("local __o =") || lua.contains("local __o="),
        "expected bound temp, lua was:\n{lua}"
    );
}