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

#include "Luau/Parser.h"
#include "Luau/StringUtils.h"
#include "Luau/Common.h"

#include <algorithm>
#include <memory>
#include <limits>
#include <math.h>


namespace
{
bool isIdentifierStartChar(char c)
{
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}

bool isDigit(char c)
{
    return c >= '0' && c <= '9';
}

bool isIdentifierChar(char c)
{
    return isIdentifierStartChar(c) || isDigit(c);
}

const std::vector<std::string> keywords = {"and",   "break", "do",  "else", "elseif", "end",    "false", "for",  "function", "if",   "in",
                                           "local", "nil",   "not", "or",   "repeat", "return", "then",  "true", "until",    "while"};

} // namespace

namespace Luau
{

struct Writer
{
    virtual ~Writer() {}

    virtual void advance(const Position&) = 0;
    virtual void newline() = 0;
    virtual void space() = 0;
    virtual void maybeSpace(const Position& newPos, int reserve) = 0;
    virtual void write(std::string_view) = 0;
    virtual void identifier(std::string_view name) = 0;
    virtual void keyword(std::string_view) = 0;
    virtual void symbol(std::string_view) = 0;
    virtual void literal(std::string_view) = 0;
    virtual void string(std::string_view) = 0;
};

struct StringWriter : Writer
{
    std::string ss;
    Position pos{0, 0};
    char lastChar = '\0'; // used to determine whether we need to inject an extra space to preserve grammatical correctness.

    const std::string& str() const
    {
        return ss;
    }

    void advance(const Position& newPos) override
    {
        while (pos.line < newPos.line)
            newline();

        if (pos.column < newPos.column)
            write(std::string(newPos.column - pos.column, ' '));
    }

    void maybeSpace(const Position& newPos, int reserve) override
    {
        if (pos.column + reserve < newPos.column)
            space();
    }

    void newline() override
    {
        ss += '\n';
        pos.column = 0;
        ++pos.line;
        lastChar = '\n';
    }

    void space() override
    {
        ss += ' ';
        ++pos.column;
        lastChar = ' ';
    }

    void write(std::string_view s) override
    {
        if (s.empty())
            return;

        ss.append(s.data(), s.size());
        pos.column += unsigned(s.size());
        lastChar = s[s.size() - 1];
    }

    void write(char c)
    {
        ss += c;
        pos.column += 1;
        lastChar = c;
    }

    void identifier(std::string_view s) override
    {
        if (s.empty())
            return;

        if (isIdentifierChar(lastChar))
            space();

        write(s);
    }

    void keyword(std::string_view s) override
    {
        if (s.empty())
            return;

        if (isIdentifierChar(lastChar))
            space();

        write(s);
    }

    void symbol(std::string_view s) override
    {
        if (isDigit(lastChar) && s[0] == '.')
            space();

        write(s);
    }

    void literal(std::string_view s) override
    {
        if (s.empty())
            return;

        else if (isIdentifierChar(lastChar) && isDigit(s[0]))
            space();

        write(s);
    }

    void string(std::string_view s) override
    {
        char quote = '\'';
        if (std::string::npos != s.find(quote))
            quote = '\"';

        write(quote);
        write(escape(s));
        write(quote);
    }
};

class CommaSeparatorInserter
{
public:
    CommaSeparatorInserter(Writer& w)
        : first(true)
        , writer(w)
    {
    }
    void operator()()
    {
        if (first)
            first = !first;
        else
            writer.symbol(",");
    }

private:
    bool first;
    Writer& writer;
};

struct Printer
{
    explicit Printer(Writer& writer)
        : writer(writer)
    {
    }

    bool writeTypes = false;
    Writer& writer;

    void visualize(const AstLocal& local)
    {
        advance(local.location.begin);

        writer.identifier(local.name.value);
        if (writeTypes && local.annotation)
        {
            writer.symbol(":");
            visualizeTypeAnnotation(*local.annotation);
        }
    }

    void visualizeTypePackAnnotation(const AstTypePack& annotation, bool forVarArg)
    {
        advance(annotation.location.begin);
        if (const AstTypePackVariadic* variadicTp = annotation.as<AstTypePackVariadic>())
        {
            if (!forVarArg)
                writer.symbol("...");

            visualizeTypeAnnotation(*variadicTp->variadicType);
        }
        else if (const AstTypePackGeneric* genericTp = annotation.as<AstTypePackGeneric>())
        {
            writer.symbol(genericTp->genericName.value);
            writer.symbol("...");
        }
        else if (const AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>())
        {
            LUAU_ASSERT(!forVarArg);
            visualizeTypeList(explicitTp->typeList, true);
        }
        else
        {
            LUAU_ASSERT(!"Unknown TypePackAnnotation kind");
        }
    }

    void visualizeTypeList(const AstTypeList& list, bool unconditionallyParenthesize)
    {
        size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0);
        if (typeCount == 0)
        {
            writer.symbol("(");
            writer.symbol(")");
        }
        else if (typeCount == 1)
        {
            if (unconditionallyParenthesize)
                writer.symbol("(");

            // Only variadic tail
            if (list.types.size == 0)
            {
                visualizeTypePackAnnotation(*list.tailType, false);
            }
            else
            {
                visualizeTypeAnnotation(*list.types.data[0]);
            }

            if (unconditionallyParenthesize)
                writer.symbol(")");
        }
        else
        {
            writer.symbol("(");

            bool first = true;
            for (const auto& el : list.types)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");

                visualizeTypeAnnotation(*el);
            }

            if (list.tailType)
            {
                writer.symbol(",");
                visualizeTypePackAnnotation(*list.tailType, false);
            }

            writer.symbol(")");
        }
    }

    bool isIntegerish(double d)
    {
        if (d <= std::numeric_limits<int>::max() && d >= std::numeric_limits<int>::min())
            return double(int(d)) == d && !(d == 0.0 && signbit(d));
        else
            return false;
    }

    void visualize(AstExpr& expr)
    {
        advance(expr.location.begin);

        if (const auto& a = expr.as<AstExprGroup>())
        {
            writer.symbol("(");
            visualize(*a->expr);
            writer.symbol(")");
        }
        else if (expr.is<AstExprConstantNil>())
        {
            writer.keyword("nil");
        }
        else if (const auto& a = expr.as<AstExprConstantBool>())
        {
            if (a->value)
                writer.keyword("true");
            else
                writer.keyword("false");
        }
        else if (const auto& a = expr.as<AstExprConstantNumber>())
        {
            if (isinf(a->value))
            {
                if (a->value > 0)
                    writer.literal("1e500");
                else
                    writer.literal("-1e500");
            }
            else if (isnan(a->value))
                writer.literal("0/0");
            else
            {
                if (isIntegerish(a->value))
                    writer.literal(std::to_string(int(a->value)));
                else
                {
                    char buffer[100];
                    size_t len = snprintf(buffer, sizeof(buffer), "%.17g", a->value);
                    writer.literal(std::string_view{buffer, len});
                }
            }
        }
        else if (const auto& a = expr.as<AstExprConstantString>())
        {
            writer.string(std::string_view(a->value.data, a->value.size));
        }
        else if (const auto& a = expr.as<AstExprLocal>())
        {
            writer.identifier(a->local->name.value);
        }
        else if (const auto& a = expr.as<AstExprGlobal>())
        {
            writer.identifier(a->name.value);
        }
        else if (expr.is<AstExprVarargs>())
        {
            writer.symbol("...");
        }
        else if (const auto& a = expr.as<AstExprCall>())
        {
            visualize(*a->func);
            writer.symbol("(");

            bool first = true;
            for (const auto& arg : a->args)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");

                visualize(*arg);
            }

            writer.symbol(")");
        }
        else if (const auto& a = expr.as<AstExprIndexName>())
        {
            visualize(*a->expr);
            writer.symbol(std::string(1, a->op));
            writer.write(a->index.value);
        }
        else if (const auto& a = expr.as<AstExprIndexExpr>())
        {
            visualize(*a->expr);
            writer.symbol("[");
            visualize(*a->index);
            writer.symbol("]");
        }
        else if (const auto& a = expr.as<AstExprFunction>())
        {
            writer.keyword("function");
            visualizeFunctionBody(*a);
        }
        else if (const auto& a = expr.as<AstExprTable>())
        {
            writer.symbol("{");

            bool first = true;

            for (const auto& item : a->items)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");

                switch (item.kind)
                {
                case AstExprTable::Item::List:
                    break;

                case AstExprTable::Item::Record:
                {
                    const auto& value = item.key->as<AstExprConstantString>()->value;
                    advance(item.key->location.begin);
                    writer.identifier(std::string_view(value.data, value.size));
                    writer.maybeSpace(item.value->location.begin, 1);
                    writer.symbol("=");
                }
                break;

                case AstExprTable::Item::General:
                {
                    writer.symbol("[");
                    visualize(*item.key);
                    writer.symbol("]");
                    writer.maybeSpace(item.value->location.begin, 1);
                    writer.symbol("=");
                }
                break;

                default:
                    LUAU_ASSERT(!"Unknown table item kind");
                }

                advance(item.value->location.begin);
                visualize(*item.value);
            }

            Position endPos = expr.location.end;
            if (endPos.column > 0)
                --endPos.column;

            advance(endPos);

            writer.symbol("}");
            advance(expr.location.end);
        }
        else if (const auto& a = expr.as<AstExprUnary>())
        {
            switch (a->op)
            {
            case AstExprUnary::Not:
                writer.keyword("not");
                break;
            case AstExprUnary::Minus:
                writer.symbol("-");
                break;
            case AstExprUnary::Len:
                writer.symbol("#");
                break;
            }
            visualize(*a->expr);
        }
        else if (const auto& a = expr.as<AstExprBinary>())
        {
            visualize(*a->left);

            switch (a->op)
            {
            case AstExprBinary::Add:
            case AstExprBinary::Sub:
            case AstExprBinary::Mul:
            case AstExprBinary::Div:
            case AstExprBinary::FloorDiv:
            case AstExprBinary::Mod:
            case AstExprBinary::Pow:
            case AstExprBinary::CompareLt:
            case AstExprBinary::CompareGt:
                writer.maybeSpace(a->right->location.begin, 2);
                writer.symbol(toString(a->op));
                break;
            case AstExprBinary::Concat:
            case AstExprBinary::CompareNe:
            case AstExprBinary::CompareEq:
            case AstExprBinary::CompareLe:
            case AstExprBinary::CompareGe:
            case AstExprBinary::Or:
                writer.maybeSpace(a->right->location.begin, 3);
                writer.keyword(toString(a->op));
                break;
            case AstExprBinary::And:
                writer.maybeSpace(a->right->location.begin, 4);
                writer.keyword(toString(a->op));
                break;
            default:
                LUAU_ASSERT(!"Unknown Op");
            }

            visualize(*a->right);
        }
        else if (const auto& a = expr.as<AstExprTypeAssertion>())
        {
            visualize(*a->expr);

            if (writeTypes)
            {
                writer.maybeSpace(a->annotation->location.begin, 2);
                writer.symbol("::");
                visualizeTypeAnnotation(*a->annotation);
            }
        }
        else if (const auto& a = expr.as<AstExprIfElse>())
        {
            writer.keyword("if");
            visualize(*a->condition);
            writer.keyword("then");
            visualize(*a->trueExpr);
            writer.keyword("else");
            visualize(*a->falseExpr);
        }
        else if (const auto& a = expr.as<AstExprInterpString>())
        {
            writer.symbol("`");

            size_t index = 0;

            for (const auto& string : a->strings)
            {
                writer.write(escape(std::string_view(string.data, string.size), /* escapeForInterpString = */ true));

                if (index < a->expressions.size)
                {
                    writer.symbol("{");
                    visualize(*a->expressions.data[index]);
                    writer.symbol("}");
                }

                index++;
            }

            writer.symbol("`");
        }
        else if (const auto& a = expr.as<AstExprError>())
        {
            writer.symbol("(error-expr");

            for (size_t i = 0; i < a->expressions.size; i++)
            {
                writer.symbol(i == 0 ? ": " : ", ");
                visualize(*a->expressions.data[i]);
            }

            writer.symbol(")");
        }
        else
        {
            LUAU_ASSERT(!"Unknown AstExpr");
        }
    }

    void writeEnd(const Location& loc)
    {
        Position endPos = loc.end;
        if (endPos.column >= 3)
            endPos.column -= 3;
        advance(endPos);
        writer.keyword("end");
    }

    void advance(const Position& newPos)
    {
        writer.advance(newPos);
    }

    void visualize(AstStat& program)
    {
        advance(program.location.begin);

        if (const auto& block = program.as<AstStatBlock>())
        {
            writer.keyword("do");
            for (const auto& s : block->body)
                visualize(*s);
            writer.advance(block->location.end);
            writeEnd(program.location);
        }
        else if (const auto& a = program.as<AstStatIf>())
        {
            writer.keyword("if");
            visualizeElseIf(*a);
        }
        else if (const auto& a = program.as<AstStatWhile>())
        {
            writer.keyword("while");
            visualize(*a->condition);
            writer.keyword("do");
            visualizeBlock(*a->body);
            writeEnd(program.location);
        }
        else if (const auto& a = program.as<AstStatRepeat>())
        {
            writer.keyword("repeat");
            visualizeBlock(*a->body);
            if (a->condition->location.begin.column > 5)
                writer.advance(Position{a->condition->location.begin.line, a->condition->location.begin.column - 6});
            writer.keyword("until");
            visualize(*a->condition);
        }
        else if (program.is<AstStatBreak>())
            writer.keyword("break");
        else if (program.is<AstStatContinue>())
            writer.keyword("continue");
        else if (const auto& a = program.as<AstStatReturn>())
        {
            writer.keyword("return");

            bool first = true;
            for (const auto& expr : a->list)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");
                visualize(*expr);
            }
        }
        else if (const auto& a = program.as<AstStatExpr>())
        {
            visualize(*a->expr);
        }
        else if (const auto& a = program.as<AstStatLocal>())
        {
            writer.keyword("local");

            bool first = true;
            for (const auto& local : a->vars)
            {
                if (first)
                    first = false;
                else
                    writer.write(",");

                visualize(*local);
            }

            first = true;
            for (const auto& value : a->values)
            {
                if (first)
                {
                    first = false;
                    writer.maybeSpace(value->location.begin, 2);
                    writer.symbol("=");
                }
                else
                    writer.symbol(",");

                visualize(*value);
            }
        }
        else if (const auto& a = program.as<AstStatFor>())
        {
            writer.keyword("for");

            visualize(*a->var);
            writer.symbol("=");
            visualize(*a->from);
            writer.symbol(",");
            visualize(*a->to);
            if (a->step)
            {
                writer.symbol(",");
                visualize(*a->step);
            }
            writer.keyword("do");
            visualizeBlock(*a->body);

            writeEnd(program.location);
        }
        else if (const auto& a = program.as<AstStatForIn>())
        {
            writer.keyword("for");

            bool first = true;
            for (const auto& var : a->vars)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");

                visualize(*var);
            }

            writer.keyword("in");

            first = true;
            for (const auto& val : a->values)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");

                visualize(*val);
            }

            writer.keyword("do");

            visualizeBlock(*a->body);

            writeEnd(program.location);
        }
        else if (const auto& a = program.as<AstStatAssign>())
        {
            bool first = true;
            for (const auto& var : a->vars)
            {
                if (first)
                    first = false;
                else
                    writer.symbol(",");
                visualize(*var);
            }

            first = true;
            for (const auto& value : a->values)
            {
                if (first)
                {
                    writer.maybeSpace(value->location.begin, 1);
                    writer.symbol("=");
                    first = false;
                }
                else
                    writer.symbol(",");

                visualize(*value);
            }
        }
        else if (const auto& a = program.as<AstStatCompoundAssign>())
        {
            visualize(*a->var);

            switch (a->op)
            {
            case AstExprBinary::Add:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("+=");
                break;
            case AstExprBinary::Sub:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("-=");
                break;
            case AstExprBinary::Mul:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("*=");
                break;
            case AstExprBinary::Div:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("/=");
                break;
            case AstExprBinary::FloorDiv:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("//=");
                break;
            case AstExprBinary::Mod:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("%=");
                break;
            case AstExprBinary::Pow:
                writer.maybeSpace(a->value->location.begin, 2);
                writer.symbol("^=");
                break;
            case AstExprBinary::Concat:
                writer.maybeSpace(a->value->location.begin, 3);
                writer.symbol("..=");
                break;
            default:
                LUAU_ASSERT(!"Unexpected compound assignment op");
            }

            visualize(*a->value);
        }
        else if (const auto& a = program.as<AstStatFunction>())
        {
            writer.keyword("function");
            visualize(*a->name);
            visualizeFunctionBody(*a->func);
        }
        else if (const auto& a = program.as<AstStatLocalFunction>())
        {
            writer.keyword("local function");
            advance(a->name->location.begin);
            writer.identifier(a->name->name.value);
            visualizeFunctionBody(*a->func);
        }
        else if (const auto& a = program.as<AstStatTypeAlias>())
        {
            if (writeTypes)
            {
                if (a->exported)
                    writer.keyword("export");

                writer.keyword("type");
                writer.identifier(a->name.value);
                if (a->generics.size > 0 || a->genericPacks.size > 0)
                {
                    writer.symbol("<");
                    CommaSeparatorInserter comma(writer);

                    for (auto o : a->generics)
                    {
                        comma();

                        writer.advance(o.location.begin);
                        writer.identifier(o.name.value);

                        if (o.defaultValue)
                        {
                            writer.maybeSpace(o.defaultValue->location.begin, 2);
                            writer.symbol("=");
                            visualizeTypeAnnotation(*o.defaultValue);
                        }
                    }

                    for (auto o : a->genericPacks)
                    {
                        comma();

                        writer.advance(o.location.begin);
                        writer.identifier(o.name.value);
                        writer.symbol("...");

                        if (o.defaultValue)
                        {
                            writer.maybeSpace(o.defaultValue->location.begin, 2);
                            writer.symbol("=");
                            visualizeTypePackAnnotation(*o.defaultValue, false);
                        }
                    }

                    writer.symbol(">");
                }
                writer.maybeSpace(a->type->location.begin, 2);
                writer.symbol("=");
                visualizeTypeAnnotation(*a->type);
            }
        }
        else if (const auto& t = program.as<AstStatTypeFunction>())
        {
            if (writeTypes)
            {
                writer.keyword("type function");
                writer.identifier(t->name.value);
                visualizeFunctionBody(*t->body);
            }
        }
        else if (const auto& a = program.as<AstStatError>())
        {
            writer.symbol("(error-stat");

            for (size_t i = 0; i < a->expressions.size; i++)
            {
                writer.symbol(i == 0 ? ": " : ", ");
                visualize(*a->expressions.data[i]);
            }

            for (size_t i = 0; i < a->statements.size; i++)
            {
                writer.symbol(i == 0 && a->expressions.size == 0 ? ": " : ", ");
                visualize(*a->statements.data[i]);
            }

            writer.symbol(")");
        }
        else
        {
            LUAU_ASSERT(!"Unknown AstStat");
        }

        if (program.hasSemicolon)
            writer.symbol(";");
    }

    void visualizeFunctionBody(AstExprFunction& func)
    {
        if (func.generics.size > 0 || func.genericPacks.size > 0)
        {
            CommaSeparatorInserter comma(writer);
            writer.symbol("<");
            for (const auto& o : func.generics)
            {
                comma();

                writer.advance(o.location.begin);
                writer.identifier(o.name.value);
            }
            for (const auto& o : func.genericPacks)
            {
                comma();

                writer.advance(o.location.begin);
                writer.identifier(o.name.value);
                writer.symbol("...");
            }
            writer.symbol(">");
        }

        writer.symbol("(");
        CommaSeparatorInserter comma(writer);

        for (size_t i = 0; i < func.args.size; ++i)
        {
            AstLocal* local = func.args.data[i];

            comma();

            advance(local->location.begin);
            writer.identifier(local->name.value);
            if (writeTypes && local->annotation)
            {
                writer.symbol(":");
                visualizeTypeAnnotation(*local->annotation);
            }
        }

        if (func.vararg)
        {
            comma();
            advance(func.varargLocation.begin);
            writer.symbol("...");

            if (func.varargAnnotation)
            {
                writer.symbol(":");
                visualizeTypePackAnnotation(*func.varargAnnotation, true);
            }
        }

        writer.symbol(")");

        if (writeTypes && func.returnAnnotation)
        {
            writer.symbol(":");
            writer.space();

            visualizeTypeList(*func.returnAnnotation, false);
        }

        visualizeBlock(*func.body);
        writeEnd(func.location);
    }

    void visualizeBlock(AstStatBlock& block)
    {
        for (const auto& s : block.body)
            visualize(*s);
        writer.advance(block.location.end);
    }

    void visualizeBlock(AstStat& stat)
    {
        if (AstStatBlock* block = stat.as<AstStatBlock>())
            visualizeBlock(*block);
        else
            LUAU_ASSERT(!"visualizeBlock was expecting an AstStatBlock");
    }

    void visualizeElseIf(AstStatIf& elseif)
    {
        visualize(*elseif.condition);
        writer.keyword("then");
        visualizeBlock(*elseif.thenbody);

        if (elseif.elsebody == nullptr)
        {
            writeEnd(elseif.location);
        }
        else if (auto elseifelseif = elseif.elsebody->as<AstStatIf>())
        {
            writer.keyword("elseif");
            visualizeElseIf(*elseifelseif);
        }
        else
        {
            writer.keyword("else");

            visualizeBlock(*elseif.elsebody);
            writeEnd(elseif.location);
        }
    }

    void visualizeTypeAnnotation(const AstType& typeAnnotation)
    {
        advance(typeAnnotation.location.begin);
        if (const auto& a = typeAnnotation.as<AstTypeReference>())
        {
            if (a->prefix)
            {
                writer.write(a->prefix->value);
                writer.symbol(".");
            }

            writer.write(a->name.value);
            if (a->parameters.size > 0 || a->hasParameterList)
            {
                CommaSeparatorInserter comma(writer);
                writer.symbol("<");
                for (auto o : a->parameters)
                {
                    comma();

                    if (o.type)
                        visualizeTypeAnnotation(*o.type);
                    else
                        visualizeTypePackAnnotation(*o.typePack, false);
                }

                writer.symbol(">");
            }
        }
        else if (const auto& a = typeAnnotation.as<AstTypeFunction>())
        {
            if (a->generics.size > 0 || a->genericPacks.size > 0)
            {
                CommaSeparatorInserter comma(writer);
                writer.symbol("<");
                for (const auto& o : a->generics)
                {
                    comma();

                    writer.advance(o.location.begin);
                    writer.identifier(o.name.value);
                }
                for (const auto& o : a->genericPacks)
                {
                    comma();

                    writer.advance(o.location.begin);
                    writer.identifier(o.name.value);
                    writer.symbol("...");
                }
                writer.symbol(">");
            }

            {
                visualizeTypeList(a->argTypes, true);
            }

            writer.symbol("->");
            visualizeTypeList(a->returnTypes, true);
        }
        else if (const auto& a = typeAnnotation.as<AstTypeTable>())
        {
            AstTypeReference* indexType = a->indexer ? a->indexer->indexType->as<AstTypeReference>() : nullptr;

            if (a->props.size == 0 && indexType && indexType->name == "number")
            {
                writer.symbol("{");
                visualizeTypeAnnotation(*a->indexer->resultType);
                writer.symbol("}");
            }
            else
            {
                CommaSeparatorInserter comma(writer);

                writer.symbol("{");

                for (std::size_t i = 0; i < a->props.size; ++i)
                {
                    comma();
                    advance(a->props.data[i].location.begin);
                    writer.identifier(a->props.data[i].name.value);
                    if (a->props.data[i].type)
                    {
                        writer.symbol(":");
                        visualizeTypeAnnotation(*a->props.data[i].type);
                    }
                }
                if (a->indexer)
                {
                    comma();
                    writer.symbol("[");
                    visualizeTypeAnnotation(*a->indexer->indexType);
                    writer.symbol("]");
                    writer.symbol(":");
                    visualizeTypeAnnotation(*a->indexer->resultType);
                }
                writer.symbol("}");
            }
        }
        else if (auto a = typeAnnotation.as<AstTypeTypeof>())
        {
            writer.keyword("typeof");
            writer.symbol("(");
            visualize(*a->expr);
            writer.symbol(")");
        }
        else if (const auto& a = typeAnnotation.as<AstTypeUnion>())
        {
            if (a->types.size == 2)
            {
                AstType* l = a->types.data[0];
                AstType* r = a->types.data[1];

                auto lta = l->as<AstTypeReference>();
                if (lta && lta->name == "nil")
                    std::swap(l, r);

                // it's still possible that we had a (T | U) or (T | nil) and not (nil | T)
                auto rta = r->as<AstTypeReference>();
                if (rta && rta->name == "nil")
                {
                    bool wrap = l->as<AstTypeIntersection>() || l->as<AstTypeFunction>();

                    if (wrap)
                        writer.symbol("(");

                    visualizeTypeAnnotation(*l);

                    if (wrap)
                        writer.symbol(")");

                    writer.symbol("?");
                    return;
                }
            }

            for (size_t i = 0; i < a->types.size; ++i)
            {
                if (i > 0)
                {
                    writer.maybeSpace(a->types.data[i]->location.begin, 2);
                    writer.symbol("|");
                }

                bool wrap = a->types.data[i]->as<AstTypeIntersection>() || a->types.data[i]->as<AstTypeFunction>();

                if (wrap)
                    writer.symbol("(");

                visualizeTypeAnnotation(*a->types.data[i]);

                if (wrap)
                    writer.symbol(")");
            }
        }
        else if (const auto& a = typeAnnotation.as<AstTypeIntersection>())
        {
            for (size_t i = 0; i < a->types.size; ++i)
            {
                if (i > 0)
                {
                    writer.maybeSpace(a->types.data[i]->location.begin, 2);
                    writer.symbol("&");
                }

                bool wrap = a->types.data[i]->as<AstTypeUnion>() || a->types.data[i]->as<AstTypeFunction>();

                if (wrap)
                    writer.symbol("(");

                visualizeTypeAnnotation(*a->types.data[i]);

                if (wrap)
                    writer.symbol(")");
            }
        }
        else if (const auto& a = typeAnnotation.as<AstTypeSingletonBool>())
        {
            writer.keyword(a->value ? "true" : "false");
        }
        else if (const auto& a = typeAnnotation.as<AstTypeSingletonString>())
        {
            writer.string(std::string_view(a->value.data, a->value.size));
        }
        else if (typeAnnotation.is<AstTypeError>())
        {
            writer.symbol("%error-type%");
        }
        else
        {
            LUAU_ASSERT(!"Unknown AstType");
        }
    }
};

std::string toString(AstNode* node)
{
    StringWriter writer;
    writer.pos = node->location.begin;

    Printer printer(writer);
    printer.writeTypes = true;

    if (auto statNode = node->asStat())
        printer.visualize(*statNode);
    else if (auto exprNode = node->asExpr())
        printer.visualize(*exprNode);
    else if (auto typeNode = node->asType())
        printer.visualizeTypeAnnotation(*typeNode);

    return writer.str();
}

void dump(AstNode* node)
{
    printf("%s\n", toString(node).c_str());
}

std::string transpile(AstStatBlock& block)
{
    StringWriter writer;
    Printer(writer).visualizeBlock(block);
    return writer.str();
}

std::string transpileWithTypes(AstStatBlock& block)
{
    StringWriter writer;
    Printer printer(writer);
    printer.writeTypes = true;
    printer.visualizeBlock(block);
    return writer.str();
}

TranspileResult transpile(std::string_view source, ParseOptions options, bool withTypes)
{
    auto allocator = Allocator{};
    auto names = AstNameTable{allocator};
    ParseResult parseResult = Parser::parse(source.data(), source.size(), names, allocator, options);

    if (!parseResult.errors.empty())
    {
        // TranspileResult keeps track of only a single error
        const ParseError& error = parseResult.errors.front();

        return TranspileResult{"", error.getLocation(), error.what()};
    }

    LUAU_ASSERT(parseResult.root);
    if (!parseResult.root)
        return TranspileResult{"", {}, "Internal error: Parser yielded empty parse tree"};

    if (withTypes)
        return TranspileResult{transpileWithTypes(*parseResult.root)};

    return TranspileResult{transpile(*parseResult.root)};
}

} // namespace Luau
