unluac 1.1.1

Multi-dialect Lua decompiler written in Rust.
Documentation
//! 输出方言选择与 best-effort 升级逻辑。
//!
//! 根据 AST 中实际出现的特性集合,决定最终输出使用的目标方言版本、
//! 代码生成模式(Strict / Permissive)以及需要向用户报告的警告。

use std::collections::BTreeSet;

use crate::ast::{
    AstDialectVersion, AstFeature, AstModule, AstTargetDialect,
    collect_ast_features, make_readable,
};
use crate::generate::GenerateMode;
use crate::timing::TimingCollector;

#[derive(Debug, Clone)]
pub(super) struct OutputPlan {
    pub readability: AstModule,
    pub target: AstTargetDialect,
    pub generate_mode: GenerateMode,
    pub warnings: Vec<String>,
}

pub(super) fn resolve_output_plan(
    ast: &AstModule,
    requested_target: AstTargetDialect,
    readability_options: crate::readability::ReadabilityOptions,
    mode: GenerateMode,
    timings: &TimingCollector,
) -> OutputPlan {
    match mode {
        GenerateMode::Strict => OutputPlan {
            readability: make_readable(
                ast,
                requested_target,
                readability_options,
                timings,
            ),
            target: requested_target,
            generate_mode: GenerateMode::Strict,
            warnings: Vec::new(),
        },
        GenerateMode::Permissive => {
            let readability = make_readable(
                ast,
                requested_target,
                readability_options,
                timings,
            );
            let unsupported = unsupported_ast_features(&readability, requested_target);
            let warnings = if unsupported.is_empty() {
                Vec::new()
            } else {
                vec![format!(
                    "requested target dialect `{}` does not support feature(s) {}; emitting permissive output.",
                    requested_target.version,
                    format_ast_features(&unsupported)
                )]
            };
            OutputPlan {
                readability,
                target: requested_target,
                generate_mode: GenerateMode::Permissive,
                warnings,
            }
        }
        GenerateMode::BestEffort => {
            let mut target =
                choose_best_effort_target(requested_target.version, &collect_ast_features(ast))
                    .unwrap_or(requested_target);

            loop {
                let readability = make_readable(
                    ast,
                    target,
                    readability_options,
                    timings,
                );
                let unsupported_in_target = unsupported_ast_features(&readability, target);
                if unsupported_in_target.is_empty() {
                    let unsupported_in_requested =
                        unsupported_ast_features(&readability, requested_target);
                    let warnings = if target.version != requested_target.version
                        && !unsupported_in_requested.is_empty()
                    {
                        vec![format!(
                            "upgraded output dialect from `{}` to `{}` to support feature(s) {}.",
                            requested_target.version,
                            target.version,
                            format_ast_features(&unsupported_in_requested)
                        )]
                    } else {
                        Vec::new()
                    };
                    return OutputPlan {
                        readability,
                        target,
                        generate_mode: GenerateMode::Strict,
                        warnings,
                    };
                }

                let final_features = collect_ast_features(&readability);
                let Some(upgraded) =
                    choose_best_effort_target(requested_target.version, &final_features)
                else {
                    let mut warnings = Vec::new();
                    let unsupported_in_requested =
                        unsupported_ast_features(&readability, requested_target);
                    if target.version != requested_target.version
                        && !unsupported_in_requested.is_empty()
                    {
                        warnings.push(format!(
                            "upgraded output dialect from `{}` to `{}` to support feature(s) {}.",
                            requested_target.version,
                            target.version,
                            format_ast_features(&unsupported_in_requested)
                        ));
                    }
                    warnings.push(format!(
                        "no single supported target dialect can express feature(s) {}; emitting permissive output.",
                        format_ast_features(&final_features)
                    ));
                    return OutputPlan {
                        readability,
                        target,
                        generate_mode: GenerateMode::Permissive,
                        warnings,
                    };
                };

                if upgraded == target {
                    let unsupported_in_requested =
                        unsupported_ast_features(&readability, requested_target);
                    let mut warnings = Vec::new();
                    if !unsupported_in_requested.is_empty() {
                        warnings.push(format!(
                            "requested target dialect `{}` does not support feature(s) {}; emitting permissive output.",
                            requested_target.version,
                            format_ast_features(&unsupported_in_requested)
                        ));
                    }
                    return OutputPlan {
                        readability,
                        target,
                        generate_mode: GenerateMode::Permissive,
                        warnings,
                    };
                }

                target = upgraded;
            }
        }
    }
}

pub(super) fn unsupported_ast_features(module: &AstModule, target: AstTargetDialect) -> BTreeSet<AstFeature> {
    collect_ast_features(module)
        .into_iter()
        .filter(|feature| !target.supports_feature(*feature))
        .collect()
}

fn choose_best_effort_target(
    requested: AstDialectVersion,
    features: &BTreeSet<AstFeature>,
) -> Option<AstTargetDialect> {
    candidate_output_versions(requested)
        .into_iter()
        .map(AstTargetDialect::new)
        .find(|target| {
            features
                .iter()
                .all(|feature| target.supports_feature(*feature))
        })
}

fn candidate_output_versions(requested: AstDialectVersion) -> Vec<AstDialectVersion> {
    match requested {
        AstDialectVersion::Lua51 => vec![
            AstDialectVersion::Lua51,
            AstDialectVersion::Lua52,
            AstDialectVersion::Lua53,
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::Lua52 => vec![
            AstDialectVersion::Lua52,
            AstDialectVersion::Lua53,
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::Lua53 => vec![
            AstDialectVersion::Lua53,
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::Lua54 => vec![
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::Lua55 => vec![
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::LuaJit => vec![
            AstDialectVersion::LuaJit,
            AstDialectVersion::Lua52,
            AstDialectVersion::Lua53,
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::Luau,
        ],
        AstDialectVersion::Luau => vec![
            AstDialectVersion::Luau,
            AstDialectVersion::Lua52,
            AstDialectVersion::Lua53,
            AstDialectVersion::Lua54,
            AstDialectVersion::Lua55,
            AstDialectVersion::LuaJit,
        ],
    }
}

fn format_ast_features(features: &BTreeSet<AstFeature>) -> String {
    features
        .iter()
        .map(|feature| feature.as_str())
        .collect::<Vec<_>>()
        .join(", ")
}

pub(super) fn ast_lowering_target(
    target: AstTargetDialect,
    mode: GenerateMode,
) -> AstTargetDialect {
    if mode == GenerateMode::Strict {
        target
    } else {
        AstTargetDialect::relaxed_for_lowering(target.version)
    }
}