root/libexec/tradcpp/directive.c
/*-
 * Copyright (c) 2010, 2013 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by David A. Holland.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

#include "bool.h"
#include "utils.h"
#include "mode.h"
#include "place.h"
#include "files.h"
#include "directive.h"
#include "macro.h"
#include "eval.h"
#include "output.h"

struct ifstate {
        struct ifstate *prev;
        struct place startplace;
        bool curtrue;
        bool evertrue;
        bool seenelse;
};

static struct ifstate *ifstate;

////////////////////////////////////////////////////////////
// common parsing bits

static
void
uncomment(char *buf)
{
        char *s, *t, *u = NULL;
        bool incomment = false;
        bool inesc = false;
        bool inquote = false;
        char quote = '\0';

        for (s = t = buf; *s; s++) {
                if (incomment) {
                        if (s[0] == '*' && s[1] == '/') {
                                s++;
                                incomment = false;
                        }
                } else {
                        if (!inquote && s[0] == '/' && s[1] == '*') {
                                incomment = true;
                        } else {
                                if (inesc) {
                                        inesc = false;
                                } else if (s[0] == '\\') {
                                        inesc = true;
                                } else if (!inquote &&
                                           (s[0] == '"' || s[0] == '\'')) {
                                        inquote = true;
                                        quote = s[0];
                                } else if (inquote && s[0] == quote) {
                                        inquote = false;
                                }

                                if (t != s) {
                                        *t = *s;
                                }
                                if (!strchr(ws, *t)) {
                                        u = t;
                                }
                                t++;
                        }
                }
        }
        if (u) {
                /* end string after last non-whitespace char */
                u[1] = '\0';
        } else {
                *t = '\0';
        }
}

static
void
oneword(const char *what, struct place *p2, char *line)
{
        size_t pos;

        pos = strcspn(line, ws);
        if (line[pos] != '\0') {
                place_addcolumns(p2, pos);
                complain(p2, "Garbage after %s argument", what);
                complain_fail();
                line[pos] = '\0';
        }
}

////////////////////////////////////////////////////////////
// if handling

static
struct ifstate *
ifstate_create(struct ifstate *prev, struct place *p, bool startstate)
{
        struct ifstate *is;

        is = domalloc(sizeof(*is));
        is->prev = prev;
        if (p != NULL) {
                is->startplace = *p;
        } else {
                place_setbuiltin(&is->startplace, 1);
        }
        is->curtrue = startstate;
        is->evertrue = is->curtrue;
        is->seenelse = false;
        return is;
}

static
void
ifstate_destroy(struct ifstate *is)
{
        dofree(is, sizeof(*is));
}

static
void
ifstate_push(struct place *p, bool startstate)
{
        struct ifstate *newstate;

        newstate = ifstate_create(ifstate, p, startstate);
        if (!ifstate->curtrue) {
                newstate->curtrue = false;
                newstate->evertrue = true;
        }
        ifstate = newstate;
}

static
void
ifstate_pop(void)
{
        struct ifstate *is;

        is = ifstate;
        ifstate = ifstate->prev;
        ifstate_destroy(is);
}

static
void
d_if(struct lineplace *lp, struct place *p2, char *line)
{
        bool doprint;
        char *expr;
        bool val;
        struct place p3 = *p2;
        size_t oldlen;

        doprint = ifstate->curtrue;

        expr = macroexpand(p2, line, strlen(line), true);

        oldlen = strlen(expr);
        uncomment(expr);
        /* trim to fit, so the malloc debugging won't complain */
        expr = dorealloc(expr, oldlen + 1, strlen(expr) + 1);

        if (ifstate->curtrue) {
                val = eval(&p3, expr);
        } else {
                val = 0;
        }
        ifstate_push(&lp->current, val);
        dostrfree(expr);

        if (doprint) {
                debuglog(&lp->current, "#if: %s",
                          ifstate->curtrue ? "taken" : "not taken");
        }
}

static
void
d_ifdef(struct lineplace *lp, struct place *p2, char *line)
{
        bool doprint;

        doprint = ifstate->curtrue;

        uncomment(line);
        oneword("#ifdef", p2, line);
        ifstate_push(&lp->current, macro_isdefined(line));

        if (doprint) {
                debuglog(&lp->current, "#ifdef %s: %s",
                         line, ifstate->curtrue ? "taken" : "not taken");
        }
}

static
void
d_ifndef(struct lineplace *lp, struct place *p2, char *line)
{
        bool doprint;

        doprint = ifstate->curtrue;

        uncomment(line);
        oneword("#ifndef", p2, line);
        ifstate_push(&lp->current, !macro_isdefined(line));

        if (doprint) {
                debuglog(&lp->current, "#ifndef %s: %s",
                         line, ifstate->curtrue ? "taken" : "not taken");
        }
}

static
void
d_elif(struct lineplace *lp, struct place *p2, char *line)
{
        bool doprint;
        char *expr;
        struct place p3 = *p2;
        size_t oldlen;

        if (ifstate->seenelse) {
                complain(&lp->current, "#elif after #else");
                complain_fail();
        }

        doprint = ifstate->curtrue;

        if (ifstate->evertrue) {
                ifstate->curtrue = false;
        } else {
                expr = macroexpand(p2, line, strlen(line), true);

                oldlen = strlen(expr);
                uncomment(expr);
                /* trim to fit, so the malloc debugging won't complain */
                expr = dorealloc(expr, oldlen + 1, strlen(expr) + 1);

                ifstate->curtrue = eval(&p3, expr);
                ifstate->evertrue = ifstate->curtrue;
                dostrfree(expr);
        }

        if (doprint) {
                debuglog2(&lp->current, &ifstate->startplace, "#elif: %s",
                          ifstate->curtrue ? "taken" : "not taken");
        }
}

static
void
d_else(struct lineplace *lp, struct place *p2, char *line)
{
        bool doprint;

        (void)p2;
        (void)line;

        if (ifstate->seenelse) {
                complain(&lp->current,
                         "Multiple #else directives in one conditional");
                complain_fail();
        }

        doprint = ifstate->curtrue;

        ifstate->curtrue = !ifstate->evertrue;
        ifstate->evertrue = true;
        ifstate->seenelse = true;

        if (doprint) {
                debuglog2(&lp->current, &ifstate->startplace, "#else: %s",
                          ifstate->curtrue ? "taken" : "not taken");
        }
}

static
void
d_endif(struct lineplace *lp, struct place *p2, char *line)
{
        (void)p2;
        (void)line;

        if (ifstate->prev == NULL) {
                complain(&lp->current, "Unmatched #endif");
                complain_fail();
        } else {
                debuglog2(&lp->current, &ifstate->startplace, "#endif");
                ifstate_pop();
        }
}

////////////////////////////////////////////////////////////
// macros

static
void
d_define(struct lineplace *lp, struct place *p2, char *line)
{
        size_t pos, argpos;
        struct place p3, p4;

        (void)lp;

        /*
         * line may be:
         *    macro expansion
         *    macro(arg, arg, ...) expansion
         */

        pos = strcspn(line, " \t\f\v(");
        if (line[pos] == '(') {
                line[pos++] = '\0';
                argpos = pos;
                pos = pos + strcspn(line+pos, "()");
                if (line[pos] == '(') {
                        place_addcolumns(p2, pos);
                        complain(p2, "Left parenthesis in macro parameters");
                        complain_fail();
                        return;
                }
                if (line[pos] != ')') {
                        place_addcolumns(p2, pos);
                        complain(p2, "Unclosed macro parameter list");
                        complain_fail();
                        return;
                }
                line[pos++] = '\0';
#if 0
                if (!strchr(ws, line[pos])) {
                        p2->column += pos;
                        complain(p2, "Trash after macro parameter list");
                        complain_fail();
                        return;
                }
#endif
        } else if (line[pos] == '\0') {
                argpos = 0;
        } else {
                line[pos++] = '\0';
                argpos = 0;
        }

        pos += strspn(line+pos, ws);

        p3 = *p2;
        place_addcolumns(&p3, argpos);

        p4 = *p2;
        place_addcolumns(&p4, pos);

        if (argpos) {
                debuglog(&lp->current, "Defining %s()", line);
                macro_define_params(p2, line, &p3,
                                    line + argpos, &p4,
                                    line + pos);
        } else {
                debuglog(&lp->current, "Defining %s", line);
                macro_define_plain(p2, line, &p4, line + pos);
        }
}

static
void
d_undef(struct lineplace *lp, struct place *p2, char *line)
{
        (void)lp;

        uncomment(line);
        oneword("#undef", p2, line);
        debuglog(&lp->current, "Undef %s", line);
        macro_undef(line);
}

////////////////////////////////////////////////////////////
// includes

static
bool
tryinclude(struct place *p, char *line)
{
        size_t len;

        len = strlen(line);
        if (len > 2 && line[0] == '"' && line[len-1] == '"') {
                line[len-1] = '\0';
                debuglog(p, "Entering include file \"%s\"", line+1);
                file_readquote(p, line+1);
                debuglog(p, "Leaving include file \"%s\"", line+1);
                line[len-1] = '"';
                return true;
        }
        if (len > 2 && line[0] == '<' && line[len-1] == '>') {
                line[len-1] = '\0';
                debuglog(p, "Entering include file <%s>", line+1);
                file_readbracket(p, line+1);
                debuglog(p, "Leaving include file <%s>", line+1);
                line[len-1] = '>';
                return true;
        }
        return false;
}

static
void
d_include(struct lineplace *lp, struct place *p2, char *line)
{
        char *text;
        size_t oldlen;

        uncomment(line);
        if (tryinclude(&lp->current, line)) {
                return;
        }
        text = macroexpand(p2, line, strlen(line), false);

        oldlen = strlen(text);
        uncomment(text);
        /* trim to fit, so the malloc debugging won't complain */
        text = dorealloc(text, oldlen + 1, strlen(text) + 1);

        if (tryinclude(&lp->current, text)) {
                dostrfree(text);
                return;
        }
        complain(&lp->current, "Illegal #include directive");
        complain(&lp->current, "Before macro expansion: #include %s", line);
        complain(&lp->current, "After macro expansion: #include %s", text);
        dostrfree(text);
        complain_fail();
}

static
void
d_line(struct lineplace *lp, struct place *p2, char *line)
{
        char *text;
        size_t oldlen;
        unsigned long val;
        char *moretext;
        size_t moretextlen;
        char *filename;

        text = macroexpand(p2, line, strlen(line), true);

        oldlen = strlen(text);
        uncomment(text);
        /* trim to fit, so the malloc debugging won't complain */
        text = dorealloc(text, oldlen + 1, strlen(text) + 1);

        /*
         * What we should have here: either 1234 "file.c",
         * or just 1234.
         */

        errno = 0;
        val = strtoul(text, &moretext, 10);
        if (errno) {
                complain(&lp->current,
                         "Invalid line number in #line directive");
                goto fail;
        }
#if UINT_MAX < ULONG_MAX
        if (val > UINT_MAX) {
                complain(&lp->current,
                         "Line number in #line directive too large");
                goto fail;
        }
#endif
        moretext += strspn(moretext, ws);
        moretextlen = strlen(moretext);
        place_addcolumns(&lp->current, moretext - text);

        if (moretextlen > 2 &&
            moretext[0] == '"' && moretext[moretextlen-1] == '"') {
                filename = dostrndup(moretext+1, moretextlen-2);
                place_changefile(&lp->nextline, filename);
                dostrfree(filename);
        }
        else if (moretextlen > 0) {
                complain(&lp->current,
                         "Invalid file name in #line directive");
                goto fail;
        }

        lp->nextline.line = val;
        dostrfree(text);
        return;

fail:
        complain(&lp->current, "Before macro expansion: #line %s", line);
        complain(&lp->current, "After macro expansion: #line %s", text);
        complain_fail();
        dostrfree(text);
}

////////////////////////////////////////////////////////////
// messages

static
void
d_warning(struct lineplace *lp, struct place *p2, char *line)
{
        char *msg;

        msg = macroexpand(p2, line, strlen(line), false);
        complain(&lp->current, "#warning: %s", msg);
        if (mode.werror) {
                complain_fail();
        }
        dostrfree(msg);
}

static
void
d_error(struct lineplace *lp, struct place *p2, char *line)
{
        char *msg;

        msg = macroexpand(p2, line, strlen(line), false);
        complain(&lp->current, "#error: %s", msg);
        complain_fail();
        dostrfree(msg);
}

////////////////////////////////////////////////////////////
// other

static
void
d_pragma(struct lineplace *lp, struct place *p2, char *line)
{
        (void)p2;

        complain(&lp->current, "#pragma %s", line);
        complain_fail();
}

////////////////////////////////////////////////////////////
// directive table

static const struct {
        const char *name;
        bool ifskip;
        void (*func)(struct lineplace *, struct place *, char *line);
} directives[] = {
        { "define",  true,  d_define },
        { "elif",    false, d_elif },
        { "else",    false, d_else },
        { "endif",   false, d_endif },
        { "error",   true,  d_error },
        { "if",      false, d_if },
        { "ifdef",   false, d_ifdef },
        { "ifndef",  false, d_ifndef },
        { "include", true,  d_include },
        { "line",    true,  d_line },
        { "pragma",  true,  d_pragma },
        { "undef",   true,  d_undef },
        { "warning", true,  d_warning },
};
static const unsigned numdirectives = HOWMANY(directives);

static
void
directive_gotdirective(struct lineplace *lp, char *line)
{
        struct place p2;
        size_t len, skip;
        unsigned i;

        p2 = lp->current;
        for (i=0; i<numdirectives; i++) {
                len = strlen(directives[i].name);
                if (!strncmp(line, directives[i].name, len) &&
                    strchr(ws, line[len])) {
                        if (directives[i].ifskip && !ifstate->curtrue) {
                                return;
                        }
                        skip = len + strspn(line+len, ws);
                        place_addcolumns(&p2, skip);
                        line += skip;

                        len = strlen(line);
                        len = notrailingws(line, len);
                        if (len < strlen(line)) {
                                line[len] = '\0';
                        }
                        directives[i].func(lp, &p2, line);
                        return;
                }
        }
        /* ugh. allow # by itself, including with a comment after it */
        uncomment(line);
        if (line[0] == '\0') {
                return;
        }

        skip = strcspn(line, ws);
        complain(&lp->current, "Unknown directive #%.*s", (int)skip, line);
        complain_fail();
}

/*
 * Check for nested comment delimiters in LINE.
 */
static
size_t
directive_scancomments(const struct lineplace *lp, char *line, size_t len)
{
        size_t pos;
        bool incomment;
        struct place p2;

        p2 = lp->current;
        incomment = 0;
        for (pos = 0; pos+1 < len; pos++) {
                if (line[pos] == '/' && line[pos+1] == '*') {
                        if (incomment) {
                                complain(&p2, "Warning: %c%c within comment",
                                         '/', '*');
                                if (mode.werror) {
                                        complain_failed();
                                }
                        } else {
                                incomment = true;
                        }
                        pos++;
                } else if (line[pos] == '*' && line[pos+1] == '/') {
                        if (incomment) {
                                incomment = false;
                        } else {
                                /* stray end-comment; should we care? */
                        }
                        pos++;
                }
                if (line[pos] == '\n') {
                        place_addlines(&p2, 1);
                        p2.column = 0;
                } else {
                        place_addcolumns(&p2, 1);
                }
        }

        /* multiline comments are supposed to arrive in a single buffer */
        assert(!incomment);
        return len;
}

void
directive_gotline(struct lineplace *lp, char *line, size_t len)
{
        size_t skip;

        if (warns.nestcomment) {
                directive_scancomments(lp, line, len);
        }

        /* check if we have a directive line (# exactly in column 0) */
        if (len > 0 && line[0] == '#') {
                skip = 1 + strspn(line + 1, ws);
                assert(skip <= len);
                place_addcolumns(&lp->current, skip);
                assert(line[len] == '\0');
                directive_gotdirective(lp, line+skip /*, length = len-skip */);
                place_addcolumns(&lp->current, len-skip);
        } else if (ifstate->curtrue) {
                macro_sendline(&lp->current, line, len);
                place_addcolumns(&lp->current, len);
        }
}

void
directive_goteof(struct place *p)
{
        while (ifstate->prev != NULL) {
                complain(p, "Missing #endif");
                complain(&ifstate->startplace, "...opened at this point");
                complain_failed();
                ifstate_pop();
        }
        macro_sendeof(p);
}

////////////////////////////////////////////////////////////
// module initialization

void
directive_init(void)
{
        ifstate = ifstate_create(NULL, NULL, true);
}

void
directive_cleanup(void)
{
        assert(ifstate->prev == NULL);
        ifstate_destroy(ifstate);
        ifstate = NULL;
}