// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "luau.pb.h"

static const std::string kNames[] = {
    "_G",
    "_VERSION",
    "__add",
    "__call",
    "__concat",
    "__div",
    "__eq",
    "__idiv",
    "__index",
    "__iter",
    "__le",
    "__len",
    "__lt",
    "__mod",
    "__mode",
    "__mul",
    "__namecall",
    "__newindex",
    "__pow",
    "__sub",
    "__type",
    "__unm",
    "abs",
    "acos",
    "arshift",
    "asin",
    "assert",
    "atan",
    "atan2",
    "band",
    "bit32",
    "bnot",
    "boolean",
    "bor",
    "btest",
    "buffer",
    "bxor",
    "byte",
    "ceil",
    "char",
    "charpattern",
    "clamp",
    "clear",
    "clock",
    "clone",
    "close",
    "codepoint",
    "codes",
    "collectgarbage",
    "concat",
    "copy",
    "coroutine",
    "cos",
    "cosh",
    "countlz",
    "countrz",
    "create",
    "date",
    "debug",
    "deg",
    "difftime",
    "error",
    "exp",
    "extract",
    "fill",
    "find",
    "floor",
    "fmod",
    "foreach",
    "foreachi",
    "format",
    "freeze",
    "frexp",
    "fromstring",
    "function",
    "gcinfo",
    "getfenv",
    "getmetatable",
    "getn",
    "gmatch",
    "gsub",
    "huge",
    "info",
    "insert",
    "ipairs",
    "isfrozen",
    "isyieldable",
    "ldexp",
    "len",
    "loadstring",
    "log",
    "log10",
    "lower",
    "lrotate",
    "lshift",
    "match",
    "math",
    "max",
    "maxn",
    "min",
    "modf",
    "move",
    "newproxy",
    "next",
    "nil",
    "noise",
    "number",
    "offset",
    "os",
    "pack",
    "packsize",
    "pairs",
    "pcall",
    "pi",
    "pow",
    "print",
    "rad",
    "random",
    "randomseed",
    "rawequal",
    "rawget",
    "rawlen",
    "rawset",
    "readf32",
    "readf64",
    "readi16",
    "readi32",
    "readi8",
    "readstring",
    "readu16",
    "readu32",
    "readu8",
    "remove",
    "rep",
    "replace",
    "require",
    "resume",
    "reverse",
    "round",
    "rrotate",
    "rshift",
    "running",
    "select",
    "setfenv",
    "setmetatable",
    "sign",
    "sin",
    "sinh",
    "sort",
    "split",
    "sqrt",
    "status",
    "string",
    "sub",
    "table",
    "tan",
    "tanh",
    "thread",
    "time",
    "tonumber",
    "tostring",
    "tostring",
    "traceback",
    "type",
    "typeof",
    "unpack",
    "upper",
    "userdata",
    "utf8",
    "vector",
    "wrap",
    "writef32",
    "writef64",
    "writei16",
    "writei32",
    "writei8",
    "writestring",
    "writeu16",
    "writeu32",
    "writeu8",
    "xpcall",
    "yield",
    "types",
    "unknown",
    "never",
    "any",
    "singleton",
    "optional",
    "generic",
    "negationof",
    "unionof",
    "intersectionof",
    "newtable",
    "newfunction",
};

static const std::string kTypes[] = {
    "any",
    "boolean",
    "buffer",
    "nil",
    "number",
    "string",
    "thread",
    "vector",
    "unknown",
    "never",
};

static const std::string kExternTypes[] = {
    "Vector3",
    "Instance",
    "Part",
};

static const std::string kBuiltinTypes[] = {
    "len", "unm", "add", "sub",   "mul",      "div",   "idiv",   "pow",          "mod",          "concat",
    "lt",  "le",  "eq",  "keyof", "rawkeyof", "index", "rawget", "setmetatable", "getmetatable",
};

struct ProtoToLuau
{
    struct Function
    {
        int loops = 0;
        bool vararg = false;
    };

    std::string source;
    std::vector<Function> functions;
    bool types = false;

    ProtoToLuau()
    {
        Function top = {};
        top.vararg = true;
        functions.push_back(top);
    }

    std::string displayName(const luau::Name& name)
    {
        if (name.has_builtin())
        {
            size_t index = size_t(name.builtin()) % std::size(kNames);
            return kNames[index];
        }
        else if (name.has_custom())
        {
            return 'n' + std::to_string(name.custom() & 0xff);
        }
        else
        {
            return "_";
        }
    }

    void ident(const luau::Name& name)
    {
        source += displayName(name);
    }

    void ident(const luau::RegularTypeName& name)
    {
        source += 't';
        source += std::to_string(name.index() & 0xff);
    }

    void ident(const luau::GenericTypeName& name)
    {
        source += char('A' + (name.index() % 26));
    }

    void ident(const luau::BuiltinTypeName& name)
    {
        size_t index = size_t(name.index()) % std::size(kBuiltinTypes);
        source += kBuiltinTypes[index];
    }

    void ident(const luau::TypeName& name)
    {
        if (name.has_regular())
            ident(name.regular());
        else if (name.has_generic())
            ident(name.generic());
        else if (name.has_builtin())
            ident(name.builtin());
        else
            source += "any";
    }

    template<typename T>
    void genericidents(const T& node)
    {
        if (node.generics_size() || node.genericpacks_size())
        {
            source += '<';
            bool first = true;

            for (size_t i = 0; i < node.generics_size(); ++i)
            {
                if (!first)
                    source += ',';
                first = false;
                ident(node.generics(i));
            }

            for (size_t i = 0; i < node.genericpacks_size(); ++i)
            {
                if (!first)
                    source += ',';
                first = false;
                ident(node.genericpacks(i));
                source += "...";
            }

            source += '>';
        }
    }

    void print(const luau::Expr& expr)
    {
        if (expr.has_group())
            print(expr.group());
        else if (expr.has_nil())
            print(expr.nil());
        else if (expr.has_bool_())
            print(expr.bool_());
        else if (expr.has_number())
            print(expr.number());
        else if (expr.has_string())
            print(expr.string());
        else if (expr.has_local())
            print(expr.local());
        else if (expr.has_global())
            print(expr.global());
        else if (expr.has_varargs())
            print(expr.varargs());
        else if (expr.has_call())
            print(expr.call());
        else if (expr.has_index_name())
            print(expr.index_name());
        else if (expr.has_index_expr())
            print(expr.index_expr());
        else if (expr.has_function())
            print(expr.function());
        else if (expr.has_table())
            print(expr.table());
        else if (expr.has_unary())
            print(expr.unary());
        else if (expr.has_binary())
            print(expr.binary());
        else if (expr.has_ifelse())
            print(expr.ifelse());
        else if (expr.has_interpstring())
            print(expr.interpstring());
        else
            source += "_";
    }

    void print(const luau::ExprPrefix& expr)
    {
        if (expr.has_group())
            print(expr.group());
        else if (expr.has_local())
            print(expr.local());
        else if (expr.has_global())
            print(expr.global());
        else if (expr.has_call())
            print(expr.call());
        else if (expr.has_index_name())
            print(expr.index_name());
        else if (expr.has_index_expr())
            print(expr.index_expr());
        else
            source += "_";
    }

    void print(const luau::ExprGroup& expr)
    {
        source += '(';
        print(expr.expr());
        source += ')';
    }

    void print(const luau::ExprConstantNil& expr)
    {
        source += "nil";
    }

    void print(const luau::ExprConstantBool& expr)
    {
        source += expr.val() ? "true" : "false";
    }

    void print(const luau::ExprConstantNumber& expr)
    {
        source += std::to_string(expr.val());
    }

    void print(const luau::ExprConstantString& expr)
    {
        source += '"';
        for (char ch : expr.val())
            if (isalpha(ch))
                source += ch;
        source += '"';
    }

    void print(const luau::Local& var)
    {
        source += 'l';
        source += std::to_string(var.name() & 0xff);
    }

    void print(const luau::ExprLocal& expr)
    {
        print(expr.var());
    }

    void print(const luau::ExprGlobal& expr)
    {
        ident(expr.name());
    }

    void print(const luau::ExprVarargs& expr)
    {
        if (functions.back().vararg)
            source += "...";
        else
            source += "_";
    }

    void print(const luau::ExprCall& expr)
    {
        if (expr.func().has_index_name())
            print(expr.func().index_name(), expr.self());
        else
            print(expr.func());
        source += '(';
        for (int i = 0; i < expr.args_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(expr.args(i));
        }
        source += ')';
    }

    void print(const luau::ExprIndexName& expr, bool self = false)
    {
        print(expr.expr());
        source += self ? ':' : '.';
        ident(expr.index());
    }

    void print(const luau::ExprIndexExpr& expr)
    {
        print(expr.expr());
        source += '[';
        print(expr.index());
        source += ']';
    }

    void function(const luau::ExprFunction& expr)
    {
        genericidents(expr);
        source += "(";
        for (int i = 0; i < expr.args_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(expr.args(i));

            if (types && i < expr.types_size())
            {
                source += ':';
                print(expr.types(i));
            }
        }
        if (expr.vararg())
        {
            if (expr.args_size())
                source += ',';
            source += "...";
        }
        source += ')';
        if (types && expr.rettypes_size())
        {
            source += ':';
            if (expr.rettypes_size() > 1)
                source += '(';
            for (size_t i = 0; i < expr.rettypes_size(); ++i)
            {
                if (i != 0)
                    source += ',';
                print(expr.rettypes(i));
            }
            if (expr.rettypes_size() > 1)
                source += ')';
        }
        source += '\n';

        Function func = {};
        func.vararg = expr.vararg();
        functions.push_back(func);

        print(expr.body());

        functions.pop_back();

        source += "end";
    }

    void print(const luau::ExprFunction& expr)
    {
        for (size_t i = 0; i < expr.attributes_size(); ++i)
        {
            print(expr.attributes(i));
            source += "\n";
        }
        source += "function";
        function(expr);
    }

    void print(const luau::ExprTable& expr)
    {
        source += '{';
        for (int i = 0; i < expr.items_size(); ++i)
        {
            if (expr.items(i).has_key_name())
            {
                ident(expr.items(i).key_name());
                source += '=';
            }
            else if (expr.items(i).has_key_expr())
            {
                source += "[";
                print(expr.items(i).key_expr());
                source += "]=";
            }

            print(expr.items(i).value());
            source += ',';
        }
        source += '}';
    }

    void print(const luau::ExprUnary& expr)
    {
        if (expr.op() == luau::ExprUnary::Not)
            source += "not ";
        else if (expr.op() == luau::ExprUnary::Minus)
            source += "- ";
        else if (expr.op() == luau::ExprUnary::Len)
            source += "# ";

        print(expr.expr());
    }

    void print(const luau::ExprBinary& expr)
    {
        print(expr.left());

        if (expr.op() == luau::ExprBinary::Add)
            source += " + ";
        else if (expr.op() == luau::ExprBinary::Sub)
            source += " - ";
        else if (expr.op() == luau::ExprBinary::Mul)
            source += " * ";
        else if (expr.op() == luau::ExprBinary::Div)
            source += " / ";
        else if (expr.op() == luau::ExprBinary::FloorDiv)
            source += " // ";
        else if (expr.op() == luau::ExprBinary::Mod)
            source += " % ";
        else if (expr.op() == luau::ExprBinary::Pow)
            source += " ^ ";
        else if (expr.op() == luau::ExprBinary::Concat)
            source += " .. ";
        else if (expr.op() == luau::ExprBinary::CompareNe)
            source += " ~= ";
        else if (expr.op() == luau::ExprBinary::CompareEq)
            source += " == ";
        else if (expr.op() == luau::ExprBinary::CompareLt)
            source += " < ";
        else if (expr.op() == luau::ExprBinary::CompareLe)
            source += " <= ";
        else if (expr.op() == luau::ExprBinary::CompareGt)
            source += " > ";
        else if (expr.op() == luau::ExprBinary::CompareGe)
            source += " >= ";
        else if (expr.op() == luau::ExprBinary::And)
            source += " and ";
        else if (expr.op() == luau::ExprBinary::Or)
            source += " or ";

        print(expr.right());
    }

    void print(const luau::ExprIfElse& expr)
    {
        source += "if ";
        print(expr.cond());
        source += " then ";
        print(expr.then());

        if (expr.has_else_())
        {
            source += " else ";
            print(expr.else_());
        }
        else if (expr.has_elseif())
        {
            source += " else";
            print(expr.elseif());
        }
    }

    void print(const luau::ExprInterpString& expr)
    {
        source += "`";

        for (int i = 0; i < expr.parts_size(); ++i)
        {
            if (expr.parts(i).has_string())
            {
                // String literal is added surrounded with "", but that's ok
                print(expr.parts(i));
            }
            else
            {
                source += "{";
                print(expr.parts(i));
                source += "}";
            }
        }

        source += "`";
    }

    void print(const luau::LValue& expr)
    {
        if (expr.has_local())
            print(expr.local());
        else if (expr.has_global())
            print(expr.global());
        else if (expr.has_index_name())
            print(expr.index_name());
        else if (expr.has_index_expr())
            print(expr.index_expr());
        else
            source += "_";
    }

    void print(const luau::Stat& stat)
    {
        if (stat.has_block())
            print(stat.block());
        else if (stat.has_if_())
            print(stat.if_());
        else if (stat.has_while_())
            print(stat.while_());
        else if (stat.has_repeat())
            print(stat.repeat());
        else if (stat.has_break_())
            print(stat.break_());
        else if (stat.has_continue_())
            print(stat.continue_());
        else if (stat.has_return_())
            print(stat.return_());
        else if (stat.has_call())
            print(stat.call());
        else if (stat.has_local())
            print(stat.local());
        else if (stat.has_for_())
            print(stat.for_());
        else if (stat.has_for_in())
            print(stat.for_in());
        else if (stat.has_assign())
            print(stat.assign());
        else if (stat.has_compound_assign())
            print(stat.compound_assign());
        else if (stat.has_function())
            print(stat.function());
        else if (stat.has_local_function())
            print(stat.local_function());
        else if (stat.has_type_alias())
            print(stat.type_alias());
        else if (stat.has_require_into_local())
            print(stat.require_into_local());
        else if (stat.has_type_function())
            print(stat.type_function());
        else
            source += "do end\n";
    }

    void print(const luau::StatBlock& stat)
    {
        for (int i = 0; i < stat.body_size(); ++i)
        {
            if (stat.body(i).has_block())
            {
                source += "do\n";
                print(stat.body(i));
                source += "end\n";
            }
            else
            {
                print(stat.body(i));

                // parser will reject code with break/continue/return being non-trailing statements in a block
                if (stat.body(i).has_break_() || stat.body(i).has_continue_() || stat.body(i).has_return_())
                    break;
            }
        }
    }

    void print(const luau::StatIf& stat)
    {
        source += "if ";
        print(stat.cond());
        source += " then\n";
        print(stat.then());

        if (stat.has_else_())
        {
            source += "else\n";
            print(stat.else_());
            source += "end\n";
        }
        else if (stat.has_elseif())
        {
            source += "else";
            print(stat.elseif());
        }
        else
        {
            source += "end\n";
        }
    }

    void print(const luau::StatWhile& stat)
    {
        source += "while ";
        print(stat.cond());
        source += " do\n";

        functions.back().loops++;
        print(stat.body());
        functions.back().loops--;

        source += "end\n";
    }

    void print(const luau::StatRepeat& stat)
    {
        source += "repeat\n";

        functions.back().loops++;
        print(stat.body());
        functions.back().loops--;

        source += "until ";
        print(stat.cond());
        source += "\n";
    }

    void print(const luau::StatBreak& stat)
    {
        if (functions.back().loops)
            source += "break\n";
        else
            source += "do end\n";
    }

    void print(const luau::StatContinue& stat)
    {
        if (functions.back().loops)
            source += "continue\n";
        else
            source += "do end\n";
    }

    void print(const luau::StatReturn& stat)
    {
        source += "return ";
        for (int i = 0; i < stat.list_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.list(i));
        }
        source += "\n";
    }

    void print(const luau::StatCall& stat)
    {
        print(stat.expr());
        source += '\n';
    }

    void print(const luau::StatLocal& stat)
    {
        source += "local ";

        if (stat.vars_size() == 0)
            source += '_';

        for (int i = 0; i < stat.vars_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.vars(i));

            if (types && i < stat.types_size())
            {
                source += ':';
                print(stat.types(i));
            }
        }

        if (stat.values_size() != 0)
            source += " = ";

        for (int i = 0; i < stat.values_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.values(i));
        }
        source += '\n';
    }

    void print(const luau::StatFor& stat)
    {
        source += "for ";
        print(stat.var());
        source += '=';
        print(stat.from());
        source += ',';
        print(stat.to());
        if (stat.has_step())
        {
            source += ',';
            print(stat.step());
        }
        source += " do\n";

        functions.back().loops++;
        print(stat.body());
        functions.back().loops--;

        source += "end\n";
    }

    void print(const luau::StatForIn& stat)
    {
        source += "for ";

        if (stat.vars_size() == 0)
            source += '_';

        for (int i = 0; i < stat.vars_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.vars(i));
        }

        source += " in ";

        if (stat.values_size() == 0)
            source += "...";

        for (int i = 0; i < stat.values_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.values(i));
        }

        source += " do\n";

        functions.back().loops++;
        print(stat.body());
        functions.back().loops--;

        source += "end\n";
    }

    void print(const luau::StatAssign& stat)
    {
        if (stat.vars_size() == 0)
            source += '_';

        for (int i = 0; i < stat.vars_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.vars(i));
        }

        source += " = ";

        if (stat.values_size() == 0)
            source += "nil";

        for (int i = 0; i < stat.values_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(stat.values(i));
        }
        source += '\n';
    }

    void print(const luau::StatCompoundAssign& stat)
    {
        print(stat.var());

        if (stat.op() == luau::StatCompoundAssign::Add)
            source += " += ";
        else if (stat.op() == luau::StatCompoundAssign::Sub)
            source += " -= ";
        else if (stat.op() == luau::StatCompoundAssign::Mul)
            source += " *= ";
        else if (stat.op() == luau::StatCompoundAssign::Div)
            source += " /= ";
        else if (stat.op() == luau::StatCompoundAssign::Mod)
            source += " %= ";
        else if (stat.op() == luau::StatCompoundAssign::Pow)
            source += " ^= ";
        else if (stat.op() == luau::StatCompoundAssign::Concat)
            source += " ..= ";

        print(stat.value());
        source += '\n';
    }

    void print(const luau::StatFunction& stat)
    {
        source += "function ";
        if (stat.var().has_index_name())
            print(stat.var().index_name(), stat.self());
        else if (stat.var().has_index_expr())
            source += '_'; // function foo[bar]() is invalid syntax
        else
            print(stat.var());
        function(stat.func());
        source += '\n';
    }

    void print(const luau::StatLocalFunction& stat)
    {
        source += "local function ";
        print(stat.var());
        function(stat.func());
        source += '\n';
    }

    void print(const luau::StatTypeAlias& stat)
    {
        if (stat.export_())
            source += "export ";

        source += "type ";
        ident(stat.name());
        genericidents(stat);
        source += " = ";
        print(stat.type());
        source += '\n';
    }

    void print(const luau::StatRequireIntoLocalHelper& stat)
    {
        source += "local ";
        print(stat.var());
        source += " = require(module" + std::to_string(stat.modulenum() % 2) + ")\n";
    }

    void print(const luau::StatTypeFunction& stat)
    {
        for (size_t i = 0; i < stat.func().attributes_size(); ++i)
        {
            print(stat.func().attributes(i));
            source += "\n";
        }

        if (stat.export_())
            source += "export ";

        source += "type function ";
        ident(stat.name());
        function(stat.func());
        source += '\n';
    }

    void print(const luau::Type& type)
    {
        if (type.has_primitive())
            print(type.primitive());
        else if (type.has_literal())
            print(type.literal());
        else if (type.has_table())
            print(type.table());
        else if (type.has_function())
            print(type.function());
        else if (type.has_typeof())
            print(type.typeof());
        else if (type.has_union_())
            print(type.union_());
        else if (type.has_intersection())
            print(type.intersection());
        else if (type.has_extern_())
            print(type.extern_());
        else if (type.has_ref())
            print(type.ref());
        else if (type.has_boolean())
            print(type.boolean());
        else if (type.has_string())
            print(type.string());
        else
            source += "any";
    }

    void print(const luau::TypePrimitive& type)
    {
        size_t index = size_t(type.kind()) % std::size(kTypes);
        source += kTypes[index];
    }

    void print(const luau::TypeLiteral& type)
    {
        ident(type.name());

        if (type.generics_size() || type.genericpacks_size())
        {
            source += '<';
            bool first = true;

            for (size_t i = 0; i < type.generics_size(); ++i)
            {
                if (!first)
                    source += ',';
                first = false;
                print(type.generics(i));
            }

            for (size_t i = 0; i < type.genericpacks_size(); ++i)
            {
                if (!first)
                    source += ',';
                first = false;
                ident(type.genericpacks(i));
                source += "...";
            }

            source += '>';
        }
    }

    void print(const luau::TableFieldAccess& expr)
    {
        if (expr == luau::TableFieldAccess::Read)
            source += "read";
        else if (expr == luau::TableFieldAccess::Write)
            source += "write";
    }

    void print(const luau::TypeTable& type)
    {
        source += '{';
        for (size_t i = 0; i < type.items_size(); ++i)
        {
            auto& item = type.items(i);

            if (item.has_access())
            {
                print(item.access());
                source += ' ';
            }

            ident(item.key());
            source += ':';
            print(item.type());
            source += ',';
        }
        if (type.has_indexer())
        {
            auto& indexer = type.indexer();

            if (indexer.has_access())
            {
                print(indexer.access());
                source += ' ';
            }

            source += '[';
            print(indexer.key());
            source += "]:";
            print(indexer.value());
        }
        source += '}';
    }

    void print(const luau::TypeFunction& type)
    {
        genericidents(type);
        source += '(';
        for (size_t i = 0; i < type.args_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(type.args(i));
        }
        source += ")->";
        if (type.rets_size() != 1)
            source += '(';
        for (size_t i = 0; i < type.rets_size(); ++i)
        {
            if (i != 0)
                source += ',';
            print(type.rets(i));
        }
        if (type.rets_size() != 1)
            source += ')';
    }

    void print(const luau::TypeTypeof& type)
    {
        source += "typeof(";
        print(type.expr());
        source += ')';
    }

    void print(const luau::TypeUnion& type)
    {
        source += '(';
        print(type.left());
        source += ")|(";
        print(type.right());
        source += ')';
    }

    void print(const luau::TypeIntersection& type)
    {
        source += '(';
        print(type.left());
        source += ")&(";
        print(type.right());
        source += ')';
    }

    void print(const luau::TypeExtern& type)
    {
        size_t index = size_t(type.kind()) % std::size(kExternTypes);
        source += kExternTypes[index];
    }

    void print(const luau::TypeRef& type)
    {
        print(type.prefix());
        source += '.';
        ident(type.index());
    }

    void print(const luau::TypeBoolean& type)
    {
        source += type.val() ? "true" : "false";
    }

    void print(const luau::TypeString& type)
    {
        source += '"';
        for (char ch : type.val())
            if (isgraph(ch))
                source += ch;
        source += '"';
    }

    void print(const luau::ExprLiteralTable& table)
    {
        source += '{';
        for (int i = 0; i < table.items_size(); ++i)
        {
            ident(table.items(i).key_name());
            source += '=';

            print(table.items(i).value());
            source += ',';
        }
        source += '}';
    }

    void print(const luau::ExprLiteral& lit)
    {
        if (lit.has_table())
            print(lit.table());
        else if (lit.has_nil())
            print(lit.nil());
        else if (lit.has_bool_())
            print(lit.bool_());
        else if (lit.has_number())
            print(lit.number());
        else if (lit.has_string())
            print(lit.string());
    }

    void print(const luau::ExprAttr& attr)
    {
        std::string name;
        if (attr.type() == luau::AttrType::Checked)
            name = "checked";
        if (attr.type() == luau::AttrType::Native)
            name = "native";
        if (attr.type() == luau::AttrType::Deprecated)
            name = "deprecated";
        else
            name = displayName(attr.name());

        if (attr.braced())
        {
            source += "@[" + name;
            if (attr.args_size() == 1)
            {
                source += " ";
                print(attr.args(0));
            }
            else if (attr.args_size() > 1)
            {
                source += "(";
                for (int i = 0; i < attr.args_size(); ++i)
                {
                    if (i != 0)
                        source += ',';
                    print(attr.args(i));
                }
                source += ")";
            }
            source += "]";
        }
        else
        {
            source += "@" + name;
        }
    }
};

std::vector<std::string> protoprint(const luau::ModuleSet& stat, bool types)
{
    std::vector<std::string> result;

    if (stat.has_module())
    {
        ProtoToLuau printer;
        printer.types = types;
        printer.print(stat.module());
        result.push_back(printer.source);
    }

    ProtoToLuau printer;
    printer.types = types;
    printer.print(stat.program());
    result.push_back(printer.source);

    return result;
}
