unluac 1.1.1

Multi-dialect Lua decompiler written in Rust.
Documentation
//! 这个文件承载 AST build 阶段“只负责合法语法化”的语法模式恢复。
//!
//! 这里的职责边界是:只把 HIR 里已经显式存在、并且能无歧义还原成某个 Lua 语法节点的
//! 形状直接降回 AST。像 `global` 这种模式,在这里仅处理字节码里确实存在对应探测/
//! 赋值序列的“显式声明”;缺失声明补全、声明合并、函数声明降糖都不属于这里,
//! 而是留给后面的 Readability。
//!
//! 例子:
//! - `LocalDecl(probe) + ErrNil + Assign(global)` 会在这里直接降成显式
//!   `AstStmt::GlobalDecl`
//! - 它不会根据一次普通 `x = ...` 写入去猜测“源码里也许应该先有 `global x`”
//! - 它也不会把 `global f = function() end` 直接美化成 `function f() end`

use crate::hir::{HirExpr, HirLValue, HirStmt};

use super::super::{AstLowerError, AstLowerer};
use crate::ast::common::{
    AstBindingRef, AstGlobalAttr, AstGlobalBinding, AstGlobalBindingTarget, AstGlobalDecl,
    AstGlobalName, AstLocalAttr, AstLocalDecl, AstStmt,
};

impl<'a> AstLowerer<'a> {
    pub(in crate::ast::build) fn try_lower_global_decl(
        &mut self,
        proto_index: usize,
        stmts: &[HirStmt],
        index: usize,
    ) -> Result<Option<(AstStmt, usize)>, AstLowerError> {
        let Some(HirStmt::LocalDecl(probe)) = stmts.get(index) else {
            return Ok(None);
        };
        let Some(HirStmt::ErrNil(err_nnil)) = stmts.get(index + 1) else {
            return Ok(None);
        };
        let Some(HirStmt::Assign(assign)) = stmts.get(index + 2) else {
            return Ok(None);
        };

        if !self.target.caps.global_decl {
            return Err(AstLowerError::UnsupportedFeature {
                dialect: self.target.version,
                feature: "global",
                context: "global declaration",
            });
        }

        if probe.bindings.len() != 1 || probe.values.len() != 1 || assign.targets.len() != 1 {
            return Ok(None);
        }
        let HirExpr::LocalRef(probe_local) = &err_nnil.value else {
            return Ok(None);
        };
        if probe.bindings[0] != *probe_local {
            return Ok(None);
        }
        if super::super::analysis::count_local_uses_in_stmts(&stmts[(index + 1)..], *probe_local)
            != 1
        {
            return Ok(None);
        }
        let Some(name) = err_nnil.name.as_ref() else {
            return Ok(None);
        };
        if !lvalue_matches_global_name(&assign.targets[0], name) {
            return Ok(None);
        }

        let values = assign
            .values
            .iter()
            .map(|value| self.lower_expr(proto_index, value))
            .collect::<Result<Vec<_>, _>>()?;
        Ok(Some((
            AstStmt::GlobalDecl(Box::new(AstGlobalDecl {
                bindings: vec![AstGlobalBinding {
                    target: AstGlobalBindingTarget::Name(AstGlobalName { text: name.clone() }),
                    attr: AstGlobalAttr::None,
                }],
                values,
            })),
            3,
        )))
    }

    pub(in crate::ast::build) fn try_lower_local_close_decl(
        &mut self,
        proto_index: usize,
        stmts: &[HirStmt],
        index: usize,
    ) -> Result<Option<(AstStmt, usize)>, AstLowerError> {
        let Some(HirStmt::LocalDecl(local_decl)) = stmts.get(index) else {
            return Ok(None);
        };
        let Some(HirStmt::ToBeClosed(to_be_closed)) = stmts.get(index + 1) else {
            return Ok(None);
        };
        let HirExpr::LocalRef(local) = &to_be_closed.value else {
            return Ok(None);
        };
        if local_decl.bindings.len() != 1 || local_decl.bindings[0] != *local {
            return Ok(None);
        }
        if !self.target.caps.local_close {
            return Err(AstLowerError::UnsupportedFeature {
                dialect: self.target.version,
                feature: "local <close>",
                context: "to-be-closed local declaration",
            });
        }
        Ok(Some((
            AstStmt::LocalDecl(Box::new(AstLocalDecl {
                bindings: vec![self.lower_local_binding(proto_index, *local, AstLocalAttr::Close)],
                values: local_decl
                    .values
                    .iter()
                    .map(|value| self.lower_expr(proto_index, value))
                    .collect::<Result<Vec<_>, _>>()?,
            })),
            2,
        )))
    }

    pub(in crate::ast::build) fn try_lower_temp_close_decl(
        &mut self,
        proto_index: usize,
        stmts: &[HirStmt],
        index: usize,
    ) -> Result<Option<(AstStmt, usize)>, AstLowerError> {
        let Some(HirStmt::Assign(assign)) = stmts.get(index) else {
            return Ok(None);
        };
        let Some(HirStmt::ToBeClosed(to_be_closed)) = stmts.get(index + 1) else {
            return Ok(None);
        };
        let HirExpr::TempRef(temp) = &to_be_closed.value else {
            return Ok(None);
        };
        if assign.targets.len() != 1 || assign.values.len() != 1 {
            return Err(AstLowerError::InvalidToBeClosed {
                proto: proto_index,
                reason: "to-be-closed temp must be introduced by a single-value assignment",
            });
        }
        let HirLValue::Temp(target) = &assign.targets[0] else {
            return Ok(None);
        };
        if target != temp {
            return Ok(None);
        }
        if !self.target.caps.local_close {
            return Err(AstLowerError::UnsupportedFeature {
                dialect: self.target.version,
                feature: "local <close>",
                context: "to-be-closed synthesized temp local",
            });
        }
        Ok(Some((
            AstStmt::LocalDecl(Box::new(AstLocalDecl {
                bindings: vec![
                    self.recovered_local_binding(AstBindingRef::Temp(*temp), AstLocalAttr::Close),
                ],
                values: vec![self.lower_expr(proto_index, &assign.values[0])?],
            })),
            2,
        )))
    }

    pub(in crate::ast::build) fn try_lower_generic_for_init(
        &mut self,
        proto_index: usize,
        stmts: &[HirStmt],
        index: usize,
        continue_target: Option<crate::ast::AstLabelId>,
    ) -> Result<Option<(AstStmt, usize)>, AstLowerError> {
        let Some(HirStmt::Assign(assign)) = stmts.get(index) else {
            return Ok(None);
        };

        let (generic_for, consumed, close_temp) = match (stmts.get(index + 1), stmts.get(index + 2))
        {
            (Some(HirStmt::ToBeClosed(to_be_closed)), Some(HirStmt::GenericFor(generic_for))) => {
                let HirExpr::TempRef(close_temp) = &to_be_closed.value else {
                    return Ok(None);
                };
                (generic_for, 3, Some(*close_temp))
            }
            (Some(HirStmt::GenericFor(generic_for)), _) => (generic_for, 2, None),
            _ => return Ok(None),
        };

        if !assign_targets_match_generic_for_init(
            assign.targets.as_slice(),
            generic_for,
            close_temp,
        ) {
            return Ok(None);
        }

        Ok(Some((
            self.lower_generic_for_stmt(
                proto_index,
                generic_for,
                Some(assign.values.as_slice()),
                continue_target,
            )?,
            consumed,
        )))
    }
}

fn lvalue_matches_global_name(target: &HirLValue, name: &str) -> bool {
    match target {
        HirLValue::Global(global) => global.name == name,
        HirLValue::TableAccess(access) => {
            matches!(&access.key, HirExpr::String(key) if key == name)
        }
        _ => false,
    }
}

fn assign_targets_match_generic_for_init(
    targets: &[HirLValue],
    generic_for: &crate::hir::HirGenericFor,
    close_temp: Option<crate::hir::TempId>,
) -> bool {
    let expected_targets = generic_for.iterator.len() + usize::from(close_temp.is_some());
    if targets.len() != expected_targets {
        return false;
    }

    let iter_targets_match =
        targets
            .iter()
            .zip(generic_for.iterator.iter())
            .all(|(target, iterator)| match (target, iterator) {
                (HirLValue::Temp(target_temp), HirExpr::TempRef(iterator_temp)) => {
                    target_temp == iterator_temp
                }
                _ => false,
            });
    if !iter_targets_match {
        return false;
    }

    match (close_temp, targets.last()) {
        (Some(close_temp), Some(HirLValue::Temp(target_temp))) => *target_temp == close_temp,
        (None, _) => true,
        _ => false,
    }
}