/**
 * Inline assembler for the GCC D compiler.
 *
 *              Copyright (C) 2018-2023 by The D Language Foundation, All Rights Reserved
 * Authors:     Iain Buclaw
 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/iasmgcc.d, _iasmgcc.d)
 * Documentation:  https://dlang.org/phobos/dmd_iasmgcc.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/iasmgcc.d
 */

module dmd.iasmgcc;

import core.stdc.string;

import dmd.arraytypes;
import dmd.astcodegen;
import dmd.dscope;
import dmd.errors;
import dmd.errorsink;
import dmd.expression;
import dmd.expressionsem;
import dmd.identifier;
import dmd.globals;
import dmd.location;
import dmd.parse;
import dmd.tokens;
import dmd.statement;
import dmd.statementsem;

private:

/***********************************
 * Parse list of extended asm input or output operands.
 * Grammar:
 *      | Operands:
 *      |     SymbolicName(opt) StringLiteral ( AssignExpression )
 *      |     SymbolicName(opt) StringLiteral ( AssignExpression ), Operands
 *      |
 *      | SymbolicName:
 *      |     [ Identifier ]
 * Params:
 *      p = parser state
 *      s = asm statement to parse
 * Returns:
 *      number of operands added to the gcc asm statement
 */
int parseExtAsmOperands(Parser)(Parser p, GccAsmStatement s)
{
    int numargs = 0;

    while (1)
    {
        Expression arg;
        Identifier name;
        Expression constraint;

        switch (p.token.value)
        {
            case TOK.semicolon:
            case TOK.colon:
            case TOK.endOfFile:
                return numargs;

            case TOK.leftBracket:
                if (p.peekNext() == TOK.identifier)
                {
                    // Skip over opening `[`
                    p.nextToken();
                    // Store the symbolic name
                    name = p.token.ident;
                    p.nextToken();
                }
                else
                {
                    p.eSink.error(s.loc, "expected identifier after `[`");
                    goto Lerror;
                }
                // Look for closing `]`
                p.check(TOK.rightBracket);
                // Look for the string literal and fall through
                if (p.token.value == TOK.string_)
                    goto case;
                else
                    goto default;

            case TOK.string_:
                constraint = p.parsePrimaryExp();
                if (p.token.value != TOK.leftParenthesis)
                {
                    arg = p.parseAssignExp();
                    error(arg.loc, "`%s` must be surrounded by parentheses", arg.toChars());
                }
                else
                {
                    // Look for the opening `(`
                    p.check(TOK.leftParenthesis);
                    // Parse the assign expression
                    arg = p.parseAssignExp();
                    // Look for the closing `)`
                    p.check(TOK.rightParenthesis);
                }

                if (!s.args)
                {
                    s.names = new Identifiers();
                    s.constraints = new Expressions();
                    s.args = new Expressions();
                }
                s.names.push(name);
                s.args.push(arg);
                s.constraints.push(constraint);
                numargs++;

                if (p.token.value == TOK.comma)
                    p.nextToken();
                break;

            default:
                p.eSink.error(p.token.loc, "expected constant string constraint for operand, not `%s`",
                        p.token.toChars());
                goto Lerror;
        }
    }
Lerror:
    while (p.token.value != TOK.rightCurly &&
           p.token.value != TOK.semicolon &&
           p.token.value != TOK.endOfFile)
        p.nextToken();

    return numargs;
}

/***********************************
 * Parse list of extended asm clobbers.
 * Grammar:
 *      | Clobbers:
 *      |     StringLiteral
 *      |     StringLiteral , Clobbers
 * Params:
 *      p = parser state
 * Returns:
 *      array of parsed clobber expressions
 */
Expressions *parseExtAsmClobbers(Parser)(Parser p)
{
    Expressions *clobbers;

    while (1)
    {
        Expression clobber;

        switch (p.token.value)
        {
            case TOK.semicolon:
            case TOK.colon:
            case TOK.endOfFile:
                return clobbers;

            case TOK.string_:
                clobber = p.parsePrimaryExp();
                if (!clobbers)
                    clobbers = new Expressions();
                clobbers.push(clobber);

                if (p.token.value == TOK.comma)
                    p.nextToken();
                break;

            default:
                p.eSink.error(p.token.loc, "expected constant string constraint for clobber name, not `%s`",
                        p.token.toChars());
                goto Lerror;
        }
    }
Lerror:
    while (p.token.value != TOK.rightCurly &&
           p.token.value != TOK.semicolon &&
           p.token.value != TOK.endOfFile)
        p.nextToken();

    return clobbers;
}

/***********************************
 * Parse list of extended asm goto labels.
 * Grammar:
 *      | GotoLabels:
 *      |     Identifier
 *      |     Identifier , GotoLabels
 * Params:
 *      p = parser state
 * Returns:
 *      array of parsed goto labels
 */
Identifiers *parseExtAsmGotoLabels(Parser)(Parser p)
{
    Identifiers *labels;

    while (1)
    {
        switch (p.token.value)
        {
            case TOK.semicolon:
            case TOK.endOfFile:
                return labels;

            case TOK.identifier:
                if (!labels)
                    labels = new Identifiers();
                labels.push(p.token.ident);

                if (p.nextToken() == TOK.comma)
                    p.nextToken();
                break;

            default:
                p.eSink.error(p.token.loc, "expected identifier for goto label name, not `%s`",
                        p.token.toChars());
                goto Lerror;
        }
    }
Lerror:
    while (p.token.value != TOK.rightCurly &&
           p.token.value != TOK.semicolon &&
           p.token.value != TOK.endOfFile)
        p.nextToken();

    return labels;
}

/***********************************
 * Parse a gcc asm statement.
 * There are three forms of inline asm statements, basic, extended, and goto.
 * Grammar:
 *      | AsmInstruction:
 *      |     BasicAsmInstruction
 *      |     ExtAsmInstruction
 *      |     GotoAsmInstruction
 *      |
 *      | BasicAsmInstruction:
 *      |     AssignExpression
 *      |
 *      | ExtAsmInstruction:
 *      |     AssignExpression : Operands(opt) : Operands(opt) : Clobbers(opt)
 *      |
 *      | GotoAsmInstruction:
 *      |     AssignExpression : : Operands(opt) : Clobbers(opt) : GotoLabels(opt)
 * Params:
 *      p = parser state
 *      s = asm statement to parse
 * Returns:
 *      the parsed gcc asm statement
 */
GccAsmStatement parseGccAsm(Parser)(Parser p, GccAsmStatement s)
{
    s.insn = p.parseAssignExp();
    if (p.token.value == TOK.semicolon || p.token.value == TOK.endOfFile)
        goto Ldone;

    // No semicolon followed after instruction template, treat as extended asm.
    foreach (section; 0 .. 4)
    {
        p.check(TOK.colon);

        final switch (section)
        {
            case 0:
                s.outputargs = p.parseExtAsmOperands(s);
                break;

            case 1:
                p.parseExtAsmOperands(s);
                break;

            case 2:
                s.clobbers = p.parseExtAsmClobbers();
                break;

            case 3:
                s.labels = p.parseExtAsmGotoLabels();
                break;
        }

        if (p.token.value == TOK.semicolon || p.token.value == TOK.endOfFile)
            goto Ldone;
    }
Ldone:
    p.check(TOK.semicolon);

    return s;
}

/***********************************
 * Parse and run semantic analysis on a GccAsmStatement.
 * Params:
 *      s  = gcc asm statement being parsed
 *      sc = the scope where the asm statement is located
 * Returns:
 *      the completed gcc asm statement, or null if errors occurred
 */
extern (C++) public Statement gccAsmSemantic(GccAsmStatement s, Scope *sc)
{
    //printf("GccAsmStatement.semantic()\n");
    scope p = new Parser!ASTCodegen(sc._module, ";", false, global.errorSink);

    // Make a safe copy of the token list before parsing.
    Token *toklist = null;
    Token **ptoklist = &toklist;

    for (Token *token = s.tokens; token; token = token.next)
    {
        *ptoklist = p.allocateToken();
        memcpy(*ptoklist, token, Token.sizeof);
        ptoklist = &(*ptoklist).next;
        *ptoklist = null;
    }
    p.token = *toklist;
    p.scanloc = s.loc;

    // Parse the gcc asm statement.
    const errors = global.errors;
    s = p.parseGccAsm(s);
    if (errors != global.errors)
        return null;
    s.stc = sc.stc;

    // Fold the instruction template string.
    s.insn = semanticString(sc, s.insn, "asm instruction template");

    if (s.labels && s.outputargs)
        s.error("extended asm statements with labels cannot have output constraints");

    // Analyse all input and output operands.
    if (s.args)
    {
        foreach (i; 0 .. s.args.length)
        {
            Expression e = (*s.args)[i];
            e = e.expressionSemantic(sc);
            // Check argument is a valid lvalue/rvalue.
            if (i < s.outputargs)
                e = e.modifiableLvalue(sc, null);
            else if (e.checkValue())
                e = ErrorExp.get();
            (*s.args)[i] = e;

            e = (*s.constraints)[i];
            e = e.expressionSemantic(sc);
            assert(e.op == EXP.string_ && (cast(StringExp) e).sz == 1);
            (*s.constraints)[i] = e;
        }
    }

    // Analyse all clobbers.
    if (s.clobbers)
    {
        foreach (i; 0 .. s.clobbers.length)
        {
            Expression e = (*s.clobbers)[i];
            e = e.expressionSemantic(sc);
            assert(e.op == EXP.string_ && (cast(StringExp) e).sz == 1);
            (*s.clobbers)[i] = e;
        }
    }

    // Analyse all goto labels.
    if (s.labels)
    {
        foreach (i; 0 .. s.labels.length)
        {
            Identifier ident = (*s.labels)[i];
            GotoStatement gs = new GotoStatement(s.loc, ident);
            if (!s.gotos)
                s.gotos = new GotoStatements();
            s.gotos.push(gs);
            gs.statementSemantic(sc);
        }
    }

    return s;
}

unittest
{
    import dmd.mtype : TypeBasic;

    if (!global.errorSink)
        global.errorSink = new ErrorSinkCompiler;

    uint errors = global.startGagging();
    scope(exit) global.endGagging(errors);

    // If this check fails, then Type._init() was called before reaching here,
    // and the entire chunk of code that follows can be removed.
    assert(ASTCodegen.Type.tint32 is null);
    // Minimally initialize the cached types in ASTCodegen.Type, as they are
    // dependencies for some fail asm tests to succeed.
    ASTCodegen.Type.stringtable._init();
    scope(exit)
    {
        ASTCodegen.Type.deinitialize();
        ASTCodegen.Type.tint32 = null;
    }
    scope tint32 = new TypeBasic(ASTCodegen.Tint32);
    ASTCodegen.Type.tint32 = tint32;

    // Imitates asmSemantic if version = IN_GCC.
    static int semanticAsm(Token* tokens)
    {
        const errors = global.errors;
        scope gas = new GccAsmStatement(Loc.initial, tokens);
        scope p = new Parser!ASTCodegen(null, ";", false, global.errorSink);
        p.token = *tokens;
        p.parseGccAsm(gas);
        return global.errors - errors;
    }

    // Imitates parseStatement for asm statements.
    static void parseAsm(string input, bool expectError)
    {
        // Generate tokens from input test.
        scope p = new Parser!ASTCodegen(null, input, false, global.errorSink);
        p.nextToken();

        Token* toklist = null;
        Token** ptoklist = &toklist;
        p.check(TOK.asm_);
        p.check(TOK.leftCurly);
        while (1)
        {
            if (p.token.value == TOK.rightCurly || p.token.value == TOK.endOfFile)
                break;
            if (p.token.value == TOK.colonColon)
            {
                *ptoklist = p.allocateToken();
                memcpy(*ptoklist, &p.token, Token.sizeof);
                (*ptoklist).value = TOK.colon;
                ptoklist = &(*ptoklist).next;

                *ptoklist = p.allocateToken();
                memcpy(*ptoklist, &p.token, Token.sizeof);
                (*ptoklist).value = TOK.colon;
                ptoklist = &(*ptoklist).next;
            }
            else
            {
                *ptoklist = p.allocateToken();
                memcpy(*ptoklist, &p.token, Token.sizeof);
                ptoklist = &(*ptoklist).next;
            }
            *ptoklist = null;
            p.nextToken();
        }
        p.check(TOK.rightCurly);

        auto res = semanticAsm(toklist);
        // Checks for both unexpected passes and failures.
        assert((res == 0) != expectError);
    }

    /// Assembly Tests, all should pass.
    /// Note: Frontend is not initialized, use only strings and identifiers.
    immutable string[] passAsmTests = [
        // Basic asm statement
        q{ asm { "nop";
        } },

        // Extended asm statement
        q{ asm { "cpuid"
               : "=a" (a), "=b" (b), "=c" (c), "=d" (d)
               : "a" (input);
        } },

        // Assembly with symbolic names
        q{ asm { "bts %[base], %[offset]"
               : [base] "+rm" (*ptr),
               : [offset] "Ir" (bitnum);
        } },

        // Assembly with clobbers
        q{ asm { "cpuid"
               : "=a" (a)
               : "a" (input)
               : "ebx", "ecx", "edx";
        } },

        // Goto asm statement
        q{ asm { "jmp %l0"
               :
               :
               :
               : Ljmplabel;
        } },

        // Any CTFE-able string allowed as instruction template.
        q{ asm { generateAsm();
        } },

        // Likewise mixins, permissible so long as the result is a string.
        q{ asm { mixin(`"repne"`, `~ "scasb"`);
        } },

        // :: token tests
        q{ asm { "" : : : "memory"; } },
        q{ asm { "" :: : "memory"; } },
        q{ asm { "" : :: "memory"; } },
        q{ asm { "" ::: "memory"; } },
    ];

    immutable string[] failAsmTests = [
        // Found 'h' when expecting ';'
        q{ asm { ""h;
        } },

        // https://issues.dlang.org/show_bug.cgi?id=20592
        q{ asm { "nop" : [name] string (expr); } },

        // Expression expected, not ';'
        q{ asm { ""[;
        } },

        // Expression expected, not ':'
        q{ asm { ""
               :
               : "g" (a ? b : : c);
        } },

        // Found ',' when expecting ':'
        q{ asm { "", "";
        } },

        // https://issues.dlang.org/show_bug.cgi?id=20593
        q{ asm { "instruction" : : "operand" 123; } },
    ];

    foreach (test; passAsmTests)
        parseAsm(test, false);

    foreach (test; failAsmTests)
        parseAsm(test, true);
}