
#define MACROS__C


/*
 Dealing with MACROs
*/

#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "macros.h"                    /* my own definitions */

#include "util.h"
#include "assemble_globals.h"
#include "assemble_aux.h"
#include "listing.h"
#include "parse.h"
#include "stream2.h"
#include "symbols.h"


/* Here's where I pretend I'm a C++ compiler.  :-/ */

/* *** derive a MACRO_STREAM from a BUFFER_STREAM with a few other args */


/* macro_stream_delete is called when a macro expansion is
   exhausted.  The unique behavior is to unwind any stacked
   conditionals.  This allows a nested .MEXIT to work.  */

void macro_stream_delete(
    STREAM *str)
{
    MACRO_STREAM   *mstr = (MACRO_STREAM *) str;

    pop_cond(mstr->cond);
    buffer_stream_delete(str);
}

STREAM_VTBL     macro_stream_vtbl = {
    macro_stream_delete, buffer_stream_getline, buffer_stream_rewind
};

STREAM         *new_macro_stream(
    STREAM *refstr,
    BUFFER *buf,
    MACRO *mac,
    int nargs)
{
    MACRO_STREAM   *mstr = memcheck(malloc(sizeof(MACRO_STREAM))); {
        char           *name = memcheck(malloc(strlen(refstr->name) + 32));

        sprintf(name, "%s:%d->%s", refstr->name, refstr->line, mac->sym.label);
        buffer_stream_construct(&mstr->bstr, buf, name);
        free(name);
    }

    mstr->bstr.stream.vtbl = &macro_stream_vtbl;
    mstr->nargs = nargs;  /* for .NARG directive: nonkeyword arguments in call*/
    mstr->cond = last_cond;
    return &mstr->bstr.stream;
}

/* read_body fetches the body of .MACRO, .REPT, .IRP, or .IRPC into a
   BUFFER. */

void read_body(
    STACK *stack,
    BUFFER *gb,
    char *name,
    int called)
{
    int             nest;

    /* Read the stream in until the end marker is hit */

    /* Note: "called" says that this body is being pulled from a macro
       library, and so under no circumstance should it be listed. */

    nest = 1;
    for (;;) {
        SYMBOL         *op;
        char           *nextline;
        char           *cp;

        nextline = stack_getline(stack);  /* Now read the line */
        if (nextline == NULL) {        /* End of file. */
            report(stack->top, "Macro body of '%s' not closed\n", name);
            break;
        }

        if (!(called & CALLED_NOLIST) &&
                (list_level - 1 + list_md) > 0) {
            list_flush();
            list_source(stack->top, nextline);
        }

        op = get_op(nextline, &cp);

        if (op == NULL) {              /* Not a pseudo-op */
            buffer_append_line(gb, nextline);
            continue;
        }
        if (op->section->type == SECTION_PSEUDO) {
            if (op->value == P_MACRO || op->value == P_REPT || op->value == P_IRP || op->value == P_IRPC) {
                nest++;
            }

            if (op->value == P_ENDM || op->value == P_ENDR) {
                nest--;
                /* If there's a name on the .ENDM, then
                 * check if the name matches the one on .MACRO.
                 * See page 7-3.
                 * Since we don't keep a stack of nested
                 * .MACRO names, just check for the outer one.
                 */
                if (nest == 0 && name && op->value == P_ENDM) {
                    cp = skipwhite(cp);
                    if (!EOL(*cp)) {
                        char           *label = get_symbol(cp, &cp, NULL);

                        if (label) {
                            if (strcmp(label, name) != 0) {
                                report(stack->top, ".ENDM '%s' does not match .MACRO '%s'\n", label, name);
                            }
                            free(label);
                        }
                    }
                }
            }

            if (nest == 0)
                return;                /* All done. */
        }

        buffer_append_line(gb, nextline);
    }
}

/* Diagnostic: dumpmacro dumps a macro definition to stdout.
   I used this for debugging; it's not called at all right now, but
   I hate to delete good code. */

void dumpmacro(
    MACRO *mac,
    FILE *fp)
{
    ARG            *arg;

    fprintf(fp, ".MACRO %s ", mac->sym.label);

    for (arg = mac->args; arg != NULL; arg = arg->next) {
        fputs(arg->label, fp);
        if (arg->value) {
            fputc('=', fp);
            fputs(arg->value, fp);
        }
        fputc(' ', fp);
    }
    fputc('\n', fp);

    fputs(mac->text->buffer, fp);

    fputs(".ENDM\n", fp);
}

/* defmacro - define a macro. */
/* Also used by .MCALL to pull macro definitions from macro libraries */

MACRO          *defmacro(
    char *cp,
    STACK *stack,
    int called)
{
    MACRO          *mac;
    ARG            *arg,
                  **argtail;
    char           *label;

    cp = skipwhite(cp);
    label = get_symbol(cp, &cp, NULL);
    if (label == NULL) {
        report(stack->top, "Invalid macro definition\n");
        return NULL;
    }

    if (!(called & CALLED_NODEFINE)) {
        /* Allow redefinition of a macro; new definition replaces the old. */
        mac = (MACRO *) lookup_sym(label, &macro_st);
        if (mac) {
            /* Remove from the symbol table... */
            remove_sym(&mac->sym, &macro_st);
            free_macro(mac);
        }
    }

    mac = new_macro(label);

    if (!(called & CALLED_NODEFINE)) {
        add_table(&mac->sym, &macro_st);
    }

    argtail = &mac->args;
    cp = skipdelim(cp);

    while (!EOL(*cp)) {
        arg = new_arg();
        arg->locsym = *cp == '?';
        if (arg->locsym) /* special argument flag? */
            cp++;
        arg->label = get_symbol(cp, &cp, NULL);
        if (arg->label == NULL) {
            /* It turns out that I have code which is badly formatted
               but which MACRO.SAV assembles.  Sigh.  */
            /* So, just quit defining arguments. */
            break;
#if 0
            report(str, "Illegal macro argument\n");
            remove_sym(&mac->sym, &macro_st);
            free_macro(mac);
            return NULL;
#endif
        }

        cp = skipwhite(cp);
        if (*cp == '=') {
            /* Default substitution given */
            arg->value = getstring(cp + 1, &cp);
            if (arg->value == NULL) {
                report(stack->top, "Illegal macro argument\n");
                remove_sym(&mac->sym, &macro_st);
                free_macro(mac);
                return NULL;
            }
        }

        /* Append to list of arguments */
        arg->next = NULL;
        *argtail = arg;
        argtail = &arg->next;

        cp = skipdelim(cp);
    }

    /* Read the stream in until the end marker is hit */  {
        BUFFER         *gb;
        int             levelmod = 0;

        gb = new_buffer();

        if ((called & CALLED_NOLIST) && !list_md) {
            list_level--;
            levelmod = 1;
        }

        read_body(stack, gb, mac->sym.label, called);

        list_level += levelmod;

        if (mac->text != NULL)         /* Discard old macro body */
            buffer_free(mac->text);

        mac->text = gb;
    }

    return mac;
}



/* Allocate a new ARG */

ARG            *new_arg(
    void)
{
    ARG            *arg = memcheck(malloc(sizeof(ARG)));

    arg->locsym = 0;
    arg->value = NULL;
    arg->next = NULL;
    arg->label = NULL;
    return arg;
}


/* Free a list of args (as for a macro, or a macro expansion) */

static void free_args(
    ARG *arg)
{
    ARG            *next;

    while (arg) {
        next = arg->next;
        if (arg->label) {
            free(arg->label);
            arg->label = NULL;
        }
        if (arg->value) {
            free(arg->value);
            arg->value = NULL;
        }
        free(arg);
        arg = next;
    }
}


/* find_arg - looks for an arg with the given name in the given
   argument list */

static ARG     *find_arg(
    ARG *arg,
    char *name)
{
    for (; arg != NULL; arg = arg->next)
        if (strcmp(arg->label, name) == 0)
            return arg;

    return NULL;
}

/* subst_args - given a BUFFER and a list of args, generate a new
   BUFFER with argument replacement having taken place. */

BUFFER         *subst_args(
    BUFFER *text,
    ARG *args)
{
    char           *in;
    char           *begin;
    BUFFER         *gb;
    char           *label;
    ARG            *arg;

    gb = new_buffer();

    /* Blindly look for argument symbols in the input. */
    /* Don't worry about quotes or comments. */

    for (begin = in = text->buffer; in < text->buffer + text->length;) {
        char           *next;

        if (issym((unsigned char)*in)) {
            label = get_symbol(in, &next, NULL);
            if (label) {
                if ((arg = find_arg(args, label))) {
                    /* An apostrophe may appear before or after the symbol. */
                    /* In either case, remove it from the expansion. */

                    if (in > begin && in[-1] == '\'')
                        in--;          /* Don't copy it. */
                    if (*next == '\'')
                        next++;

                    /* Copy prior characters */
                    buffer_appendn(gb, begin, (int) (in - begin));
                    /* Copy replacement string */
                    buffer_append_line(gb, arg->value);
                    in = begin = next;
                    --in;              /* prepare for subsequent increment */
                }
                free(label);
                in = next;
            } else
                in++;
        } else
            in++;
    }

    /* Append the rest of the text */
    buffer_appendn(gb, begin, (int) (in - begin));

    return gb;                         /* Done. */
}

/* eval_str - the language allows an argument expression to be given
   as "\expression" which means, evaluate the expression and
   substitute the numeric value in the current radix. */

char *eval_str(
    STREAM *refstr,
    char *arg)
{
    EX_TREE        *value = parse_expr(arg, 0);
    unsigned        word = 0;
    char            temp[10];

    if (value->type != EX_LIT) {
        report(refstr, "Constant value required\n");
    } else
        word = value->data.lit;

    free_tree(value);

    /* printf can't do base 2. */
    my_ultoa(word & 0177777, temp, radix);
    free(arg);
    arg = memcheck(strdup(temp));
    return arg;
}

/* getstring_macarg - parse a string that possibly starts with a backslash.
 * If so, performs expression evaluation.
 *
 * The current implementation over-accepts some input.
 *
 * MACRO V05.05:

        .list me
        .macro test x
        .blkb x
        .endm

        size = 10
        foo = 2

    ; likes:

        test size
        test \size
        test \<size>
        test \<size + foo>
        test ^/size + foo/

    ; dislikes:

        test <\size>          ; arg is \size which could be ok in other cases
        test size + foo       ; gets split at the space
        test /size + foo/     ; gets split at the space
        test \/size + foo/
        test \^/size + foo/   ; * accepted by this version

 */

char           *getstring_macarg(
    STREAM *refstr,
    char *cp,
    char **endp)
{
    if (cp[0] == '\\') {
        char *str = getstring(cp + 1, endp);
        return eval_str(refstr, str);         /* Perform expression evaluation */
    } else {
        return getstring(cp, endp);
    }
}

/* expandmacro - return a STREAM containing the expansion of a macro */

STREAM         *expandmacro(
    STREAM *refstr,
    MACRO *mac,
    char *cp)
{
    ARG            *arg,
                   *args,
                   *macarg;
    char           *label;
    STREAM         *str;
    BUFFER         *buf;
    int             nargs;
    int             onemore;

    args = NULL;
    arg = NULL;
    nargs = 0;      /* for the .NARG directive */
    onemore = 0;

    /* Parse the arguments */

    while (!EOL(*cp) || onemore) {
        char           *nextcp;

        /* Check for named argument */
        label = get_symbol(cp, &nextcp, NULL);
        if (label && (nextcp = skipwhite(nextcp), *nextcp == '=') && (macarg = find_arg(mac->args, label))) {
            /* Check if I've already got a value for it */
            if ((arg = find_arg(args, label)) != NULL) {
                /* Duplicate is legal; the last one wins. */
                if (arg->value) {
                    free (arg->value);
                    arg->value = NULL;
                }
            }
            else {
                arg = new_arg();
                arg->label = label;
                arg->next = args;
                args = arg;
            }
            nextcp = skipwhite(nextcp + 1);
            arg->value = getstring_macarg(refstr, nextcp, &nextcp);
        } else {
            if (label)
                free(label);

            /* Find correct positional argument */

            for (macarg = mac->args; macarg != NULL; macarg = macarg->next) {
                if (find_arg(args, macarg->label) == NULL)
                    break;             /* This is the next positional arg */
            }

            if (macarg == NULL)
                break;                 /* Don't pick up any more arguments. */

            nextcp = skipwhite (cp);
            arg = new_arg();
            arg->label = memcheck(strdup(macarg->label));       /* Copy the name */
            arg->next = args;
            args = arg;
            if (*nextcp != ',') {
                arg->value = getstring_macarg(refstr, cp, &nextcp);
            }
            else {
                arg->value = NULL;
            }
            nargs++;                   /* Count nonkeyword arguments only. */
        }

        /* If there is a trailing comma, there is an empty last argument */
        cp = skipdelim_comma(nextcp, &onemore);
    }

    /* Now go back and fill in defaults */  {
        int             locsym;

        if (last_macro_lsb != lsb)
            locsym = last_locsym = 32768;
        else
            locsym = last_locsym;
        last_macro_lsb = lsb;

        for (macarg = mac->args; macarg != NULL; macarg = macarg->next) {
            arg = find_arg(args, macarg->label);
            if (arg == NULL || arg->value == NULL) {
                int wasnull = 0;
                if (arg == NULL) {
                    wasnull = 1;
                    arg = new_arg();
                    arg->label = memcheck(strdup(macarg->label));
                }
                if (macarg->locsym) {
                    char            temp[32];

                    /* Here's where we generate local labels */
                    sprintf(temp, "%d$", locsym++);
                    arg->value = memcheck(strdup(temp));
                } else if (macarg->value) {
                    arg->value = memcheck(strdup(macarg->value));
                } else
                    arg->value = memcheck(strdup(""));

                if (wasnull) {
                    arg->next = args;
                    args = arg;
                }
            }
        }

        last_locsym = locsym;
    }

    buf = subst_args(mac->text, args);

    str = new_macro_stream(refstr, buf, mac, nargs);

    free_args(args);
    buffer_free(buf);

    return str;
}


/* dump_all_macros is a diagnostic function that's currently not
   used.  I used it while debugging, and I haven't removed it. */

void dump_all_macros(
    void)
{
    MACRO          *mac;
    SYMBOL_ITER     iter;

    for (mac = (MACRO *) first_sym(&macro_st, &iter); mac != NULL; mac = (MACRO *) next_sym(&macro_st, &iter)) {
        dumpmacro(mac, lstfile);

        fprintf(lstfile, "\n\n");
    }
}


/* Allocate a new macro */

MACRO          *new_macro(
    char *label)
{
    MACRO          *mac = memcheck(malloc(sizeof(MACRO)));

    mac->sym.flags = SYMBOLFLAG_DEFINITION;
    mac->sym.label = label;
    mac->sym.stmtno = stmtno;
    mac->sym.next = NULL;
    mac->sym.section = &macro_section;
    mac->sym.value = 0;
    mac->args = NULL;
    mac->text = NULL;

    return mac;
}

/* free a macro, its args, its text, etc. */
void free_macro(
    MACRO *mac)
{
    if (mac->text) {
        free(mac->text);
    }
    free_args(mac->args);
    free_sym(&mac->sym);
}

int do_mcall (char *label, STACK *stack)
{
    SYMBOL         *op;         /* The operation SYMBOL */
    STREAM         *macstr;
    BUFFER         *macbuf;
    char           *maccp;
    int             saveline;
    MACRO          *mac;
    int             i;
    char            macfile[FILENAME_MAX];
    char            hitfile[FILENAME_MAX];

    /* Find the macro in the list of included
       macro libraries */
    macbuf = NULL;
    for (i = 0; i < nr_mlbs; i++)
        if ((macbuf = mlb_entry(mlbs[i], label)) != NULL)
            break;
    if (macbuf != NULL) {
        macstr = new_buffer_stream(macbuf, label);
        buffer_free(macbuf);
    } else {
        char *bufend = &macfile[sizeof(macfile)],
                       *end;
        end = stpncpy(macfile, label, sizeof(macfile) - 5);
        if (end >= bufend - 5) {
            report(stack->top, ".MCALL: name too long: '%s'\n", label);
            return 0;
        }
        stpncpy(end, ".MAC", bufend - end);
        my_searchenv(macfile, "MCALL", hitfile, sizeof(hitfile));
        if (hitfile[0])
            macstr = new_file_stream(hitfile);
        else
            macstr = NULL;
    }

    if (macstr != NULL) {
        for (;;) {
            char           *mlabel;

            maccp = macstr->vtbl->getline(macstr);
            if (maccp == NULL)
                break;
            mlabel = get_symbol(maccp, &maccp, NULL);
            if (mlabel == NULL)
                continue;
            op = lookup_sym(mlabel, &system_st);
            free(mlabel);
            if (op == NULL)
                continue;
            if (op->value == P_MACRO)
                break;
        }

        if (maccp != NULL) {
            STACK           macstack = {
                macstr
            };
            int             savelist = list_level;

            saveline = stmtno;
            list_level = -1;
            mac = defmacro(maccp, &macstack, CALLED_NOLIST);
            if (mac == NULL) {
                report(stack->top, "Failed to define macro called %s\n", label);
            }

            stmtno = saveline;
            list_level = savelist;
        }

        macstr->vtbl->delete(macstr);
        return 1;
    }
    return 0;
}
