unluac 1.1.1

Multi-dialect Lua decompiler written in Rust.
Documentation
use crate::hir::{
    HirAssign, HirBlock, HirClose, HirExpr, HirIf, HirLValue, HirProto, HirProtoRef, HirReturn,
    HirStmt, HirToBeClosed, TempId,
};
use crate::parser::{ProtoLineRange, ProtoSignature};

use super::materialize_tbc_close_scopes_in_proto;

#[test]
fn materializes_simple_tbc_region_into_block() {
    let mut proto = empty_proto(vec![
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(0))],
            values: vec![HirExpr::Integer(1)],
        })),
        HirStmt::ToBeClosed(Box::new(HirToBeClosed {
            reg_index: 2,
            value: HirExpr::TempRef(TempId(0)),
        })),
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(1))],
            values: vec![HirExpr::Integer(2)],
        })),
        HirStmt::Close(Box::new(HirClose { from_reg: 2 })),
        HirStmt::Return(Box::new(crate::hir::HirReturn { values: Vec::new() })),
    ]);

    assert!(materialize_tbc_close_scopes_in_proto(&mut proto));

    assert!(matches!(
        proto.body.stmts.as_slice(),
        [HirStmt::Block(_), HirStmt::Return(_)]
    ));
    let HirStmt::Block(block) = &proto.body.stmts[0] else {
        panic!("expected materialized block");
    };
    assert!(matches!(
        block.stmts.as_slice(),
        [
            HirStmt::Assign(_),
            HirStmt::ToBeClosed(_),
            HirStmt::Assign(_)
        ]
    ));
}

#[test]
fn removes_all_matching_close_markers_inside_nested_tbc_scope() {
    let mut proto = empty_proto(vec![
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(0))],
            values: vec![HirExpr::Integer(1)],
        })),
        HirStmt::ToBeClosed(Box::new(HirToBeClosed {
            reg_index: 3,
            value: HirExpr::TempRef(TempId(0)),
        })),
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(1))],
            values: vec![HirExpr::Integer(2)],
        })),
        HirStmt::ToBeClosed(Box::new(HirToBeClosed {
            reg_index: 4,
            value: HirExpr::TempRef(TempId(1)),
        })),
        HirStmt::Close(Box::new(HirClose { from_reg: 4 })),
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(2))],
            values: vec![HirExpr::Integer(3)],
        })),
        HirStmt::Close(Box::new(HirClose { from_reg: 4 })),
        HirStmt::Close(Box::new(HirClose { from_reg: 3 })),
    ]);

    assert!(materialize_tbc_close_scopes_in_proto(&mut proto));

    let HirStmt::Block(outer) = &proto.body.stmts[0] else {
        panic!("expected outer block");
    };
    assert!(
        outer
            .stmts
            .iter()
            .all(|stmt| !matches!(stmt, HirStmt::Close(_))),
        "outer scope should not retain matching close markers"
    );
}

#[test]
fn materializes_scope_when_close_lives_in_child_branch() {
    let mut proto = empty_proto(vec![
        HirStmt::Assign(Box::new(HirAssign {
            targets: vec![HirLValue::Temp(TempId(0))],
            values: vec![HirExpr::Integer(1)],
        })),
        HirStmt::ToBeClosed(Box::new(HirToBeClosed {
            reg_index: 3,
            value: HirExpr::TempRef(TempId(0)),
        })),
        HirStmt::If(Box::new(HirIf {
            cond: HirExpr::Boolean(true),
            then_block: HirBlock {
                stmts: vec![
                    HirStmt::Close(Box::new(HirClose { from_reg: 3 })),
                    HirStmt::Return(Box::new(HirReturn {
                        values: vec![HirExpr::TempRef(TempId(0))],
                    })),
                ],
            },
            else_block: Some(HirBlock {
                stmts: vec![HirStmt::Return(Box::new(HirReturn { values: Vec::new() }))],
            }),
        })),
    ]);

    assert!(materialize_tbc_close_scopes_in_proto(&mut proto));

    let [HirStmt::Block(block)] = proto.body.stmts.as_slice() else {
        panic!("expected tbc scope to materialize as a single block");
    };
    let [
        HirStmt::Assign(_),
        HirStmt::ToBeClosed(_),
        HirStmt::If(if_stmt),
    ] = block.stmts.as_slice()
    else {
        panic!("expected block to keep decl/tbc/if shape");
    };
    assert!(
        if_stmt
            .then_block
            .stmts
            .iter()
            .all(|stmt| !matches!(stmt, HirStmt::Close(_))),
        "child branch should not retain matching close marker",
    );
}

fn empty_proto(stmts: Vec<HirStmt>) -> HirProto {
    HirProto {
        id: HirProtoRef(0),
        source: None,
        line_range: ProtoLineRange {
            defined_start: 0,
            defined_end: 0,
        },
        signature: ProtoSignature {
            num_params: 0,
            is_vararg: false,
            has_vararg_param_reg: false,
            named_vararg_table: false,
        },
        params: Vec::new(),
        param_debug_hints: Vec::new(),
        locals: Vec::new(),
        local_debug_hints: Vec::new(),
        upvalues: Vec::new(),
        upvalue_debug_hints: Vec::new(),
        temps: vec![TempId(0), TempId(1), TempId(2)],
        temp_debug_locals: Vec::new(),
        body: HirBlock { stmts },
        children: Vec::new(),
    }
}