| /* |
| ** 2022-11-12: |
| ** |
| ** The author disclaims copyright to this source code. In place of |
| ** a legal notice, here is a blessing: |
| ** |
| ** * May you do good and not evil. |
| ** * May you find forgiveness for yourself and forgive others. |
| ** * May you share freely, never taking more than you give. |
| ** |
| ************************************************************************ |
| ** |
| ** The C-minus Preprocessor: a truly minimal C-like preprocessor. |
| ** Why? Because C preprocessors _can_ process non-C code but generally make |
| ** quite a mess of it. The purpose of this application is an extremely |
| ** minimal preprocessor with only the most basic functionality of a C |
| ** preprocessor, namely: |
| ** |
| ** - Limited `#if`, where its one argument is a macro name which |
| ** resolves to true if it's defined, false if it's not. Likewise, |
| ** `#ifnot` is the inverse. Includes `#else` and `#elif` and |
| ** `#elifnot`. Such chains are terminated with `#endif`. |
| ** |
| ** - `#define` accepts one or more arguments, the names of |
| ** macros. Each one is implicitly true. |
| ** |
| ** - `#undef` undefine one or more macros. |
| ** |
| ** - `#error` treats the rest of the line as a fatal error message. |
| ** |
| ** - `#include` treats its argument as a filename token (NOT quoted, |
| ** though support for quoting may be added later). Some effort is |
| ** made to prevent recursive inclusion, but that support is both |
| ** somewhat fragile and possibly completely unnecessary. |
| ** |
| ** - `#pragma` is in place for adding "meta-commands", but it does not |
| ** yet have any concrete list of documented commands. |
| ** |
| * - `#stderr` outputs its file name, line number, and the remaininder |
| ** of that line to stderr. |
| ** |
| ** - `#//` acts as a single-line comment, noting that there must be as |
| ** space after the `//` part because `//` is (despite appearances) |
| ** parsed like a keyword. |
| ** |
| ** Note that "#" above is symbolic. The keyword delimiter is |
| ** configurable and defaults to "##". Define CMPP_DEFAULT_DELIM to a |
| ** string when compiling to define the default at build-time. |
| ** |
| ** This preprocessor does no expansion of content except within the |
| ** bounds of its `#keywords`. |
| ** |
| ** Design note: this code makes use of sqlite3. Though not _strictly_ |
| ** needed in order to implement it, this tool was specifically created |
| ** for use with the sqlite3 project's own JavaScript code, so there's |
| ** no reason not to make use of it to do some of the heavy lifting. It |
| ** does not require any cutting-edge sqlite3 features and should be |
| ** usable with any version which supports `WITHOUT ROWID`. |
| ** |
| ** Author(s): |
| ** |
| ** - Stephan Beal <https://wanderinghorse.net/home/stephan/> |
| */ |
| |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <errno.h> |
| #include <string.h> |
| #include <stdarg.h> |
| #include <assert.h> |
| #include <ctype.h> |
| |
| #include "sqlite3.h" |
| |
| #if defined(_WIN32) || defined(WIN32) |
| # include <io.h> |
| # include <fcntl.h> |
| # ifndef access |
| # define access(f,m) _access((f),(m)) |
| # endif |
| #else |
| # include <unistd.h> |
| #endif |
| |
| #ifndef CMPP_DEFAULT_DELIM |
| #define CMPP_DEFAULT_DELIM "##" |
| #endif |
| |
| #if 1 |
| # define CMPP_NORETURN __attribute__((noreturn)) |
| #else |
| # define CMPP_NORETURN |
| #endif |
| |
| /* Fatally exits the app with the given printf-style message. */ |
| static CMPP_NORETURN void fatalv(char const *zFmt, va_list); |
| static CMPP_NORETURN void fatal(char const *zFmt, ...); |
| |
| /** Proxy for free(), for symmetry with cmpp_realloc(). */ |
| static void cmpp_free(void *p); |
| /** A realloc() proxy which dies fatally on allocation error. */ |
| static void * cmpp_realloc(void * p, unsigned n); |
| #if 0 |
| /** A malloc() proxy which dies fatally on allocation error. */ |
| static void * cmpp_malloc(unsigned n); |
| #endif |
| |
| /* |
| ** If p is stdin or stderr then this is a no-op, else it is a |
| ** proxy for fclose(). This is a no-op if p is NULL. |
| */ |
| static void FILE_close(FILE *p); |
| /* |
| ** Works like fopen() but accepts the special name "-" to mean either |
| ** stdin (if zMode indicates a real-only mode) or stdout. Fails |
| ** fatally on error. |
| */ |
| static FILE * FILE_open(char const *zName, const char * zMode); |
| /* |
| ** Reads the entire contents of the given file, allocating it in a |
| ** buffer which gets assigned to `*pOut`. `*nOut` gets assigned the |
| ** length of the output buffer. Fails fatally on error. |
| */ |
| static void FILE_slurp(FILE *pFile, unsigned char **pOut, |
| unsigned * nOut); |
| |
| /* |
| ** Intended to be passed an sqlite3 result code. If it's non-0 |
| ** then it emits a fatal error message which contains both the |
| ** given string and the sqlite3_errmsg() from the application's |
| ** database instance. |
| */ |
| static void db_affirm_rc(int rc, const char * zMsg); |
| |
| /* |
| ** Proxy for sqlite3_str_finish() which fails fatally if that |
| ** routine returns NULL. |
| */ |
| static char * db_str_finish(sqlite3_str *s, int * n); |
| /* |
| ** Proxy for sqlite3_str_new() which fails fatally if that |
| ** routine returns NULL. |
| */ |
| static sqlite3_str * db_str_new(void); |
| |
| /* Proxy for sqlite3_finalize(). */ |
| static void db_finalize(sqlite3_stmt *pStmt); |
| /* |
| ** Proxy for sqlite3_step() which fails fatally if the result |
| ** is anything other than SQLITE_ROW or SQLITE_DONE. |
| */ |
| static int db_step(sqlite3_stmt *pStmt); |
| /* |
| ** Proxy for sqlite3_bind_int() which fails fatally on error. |
| */ |
| static void db_bind_int(sqlite3_stmt *pStmt, int col, int val); |
| #if 0 |
| /* |
| ** Proxy for sqlite3_bind_null() which fails fatally on error. |
| */ |
| static void db_bind_null(sqlite3_stmt *pStmt, int col); |
| #endif |
| /* |
| ** Proxy for sqlite3_bind_text() which fails fatally on error. |
| */ |
| static void db_bind_text(sqlite3_stmt *pStmt, int col, const char * zStr); |
| /* |
| ** Proxy for sqlite3_bind_text() which fails fatally on error. |
| */ |
| static void db_bind_textn(sqlite3_stmt *pStmt, int col, const char * zStr, int len); |
| #if 0 |
| /* |
| ** Proxy for sqlite3_bind_text() which fails fatally on error. It uses |
| ** sqlite3_str_vappendf() so supports all of its formatting options. |
| */ |
| static void db_bind_textv(sqlite3_stmt *pStmt, int col, const char * zFmt, ...); |
| #endif |
| /* |
| ** Proxy for sqlite3_free(), to be passed any memory which is allocated |
| ** by sqlite3_malloc(). |
| */ |
| static void db_free(void *m); |
| /* |
| ** Adds the given `#define` macro name to the list of macros, ignoring |
| ** any duplicates. Fails fatally on error. |
| */ |
| static void db_define_add(const char * zKey); |
| /* |
| ** Returns true if the given key is already in the `#define` list, |
| ** else false. Fails fatally on db error. |
| */ |
| static int db_define_has(const char * zName); |
| /* |
| ** Removes the given `#define` macro name from the list of |
| ** macros. Fails fatally on error. |
| */ |
| static void db_define_rm(const char * zKey); |
| /* |
| ** Adds the given filename to the list of being-`#include`d files, |
| ** using the given source file name and line number of error reporting |
| ** purposes. If recursion is later detected. |
| */ |
| static void db_including_add(const char * zKey, const char * zSrc, int srcLine); |
| /* |
| ** Adds the given dir to the list of includes. They are checked in the |
| ** order they are added. |
| */ |
| static void db_include_dir_add(const char * zKey); |
| /* |
| ** Returns a resolved path of PREFIX+'/'+zKey, where PREFIX is one of |
| ** the `#include` dirs (db_include_dir_add()). If no file match is |
| ** found, NULL is returned. Memory must eventually be passed to |
| ** db_free() to free it. |
| */ |
| static char * db_include_search(const char * zKey); |
| /* |
| ** Removes the given key from the `#include` list. |
| */ |
| static void db_include_rm(const char * zKey); |
| /* |
| ** A proxy for sqlite3_prepare() which fails fatally on error. |
| */ |
| static void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...); |
| |
| /* |
| ** Opens the given file and processes its contents as c-pp, sending |
| ** all output to the global c-pp output channel. Fails fatally on |
| ** error. |
| */ |
| static void cmpp_process_file(const char * zName); |
| |
| /* |
| ** Returns the number newline characters between the given starting |
| ** point and inclusive ending point. Results are undefined if zFrom is |
| ** greater than zTo. |
| */ |
| static unsigned count_lines(unsigned char const * zFrom, |
| unsigned char const *zTo); |
| |
| /* |
| ** Wrapper around a FILE handle. |
| */ |
| struct FileWrapper { |
| /* File's name. */ |
| char const *zName; |
| /* FILE handle. */ |
| FILE * pFile; |
| /* Where FileWrapper_slurp() stores the file's contents. */ |
| unsigned char * zContent; |
| /* Size of this->zContent, as set by FileWrapper_slurp(). */ |
| unsigned nContent; |
| }; |
| typedef struct FileWrapper FileWrapper; |
| #define FileWrapper_empty_m {0,0,0,0} |
| static const FileWrapper FileWrapper_empty = FileWrapper_empty_m; |
| |
| /* Proxy for FILE_close(). */ |
| static void FileWrapper_close(FileWrapper * p); |
| /* Proxy for FILE_open(). */ |
| static void FileWrapper_open(FileWrapper * p, const char * zName, const char *zMode); |
| /* Proxy for FILE_slurp(). */ |
| static void FileWrapper_slurp(FileWrapper * p); |
| |
| /* |
| ** Outputs a printf()-formatted message to stderr. |
| */ |
| static void g_stderr(char const *zFmt, ...); |
| /* |
| ** Outputs a printf()-formatted message to stderr. |
| */ |
| static void g_stderrv(char const *zFmt, va_list); |
| #define g_debug(lvl,pfexpr) \ |
| if(lvl<=g.doDebug) g_stderr("%s @ %s:%d: ",g.zArgv0,__FILE__,__LINE__); \ |
| if(lvl<=g.doDebug) g_stderr pfexpr |
| |
| void fatalv(char const *zFmt, va_list va){ |
| if(zFmt && *zFmt){ |
| vfprintf(stderr, zFmt, va); |
| } |
| fputc('\n', stderr); |
| exit(1); |
| } |
| |
| void fatal(char const *zFmt, ...){ |
| va_list va; |
| va_start(va, zFmt); |
| fatalv(zFmt, va); |
| va_end(va); |
| } |
| |
| void cmpp_free(void *p){ |
| free(p); |
| } |
| |
| void * cmpp_realloc(void * p, unsigned n){ |
| void * const rc = realloc(p, n); |
| if(!rc) fatal("realloc(P,%u) failed", n); |
| return rc; |
| } |
| |
| #if 0 |
| void * cmpp_malloc(unsigned n){ |
| void * const rc = malloc(n); |
| if(!rc) fatal("malloc(%u) failed", n); |
| return rc; |
| } |
| #endif |
| |
| FILE * FILE_open(char const *zName, const char * zMode){ |
| FILE * p; |
| if('-'==zName[0] && 0==zName[1]){ |
| p = strstr(zMode,"w") ? stdout : stdin; |
| }else{ |
| p = fopen(zName, zMode); |
| if(!p) fatal("Cannot open file [%s] with mode [%s]", zName, zMode); |
| } |
| return p; |
| } |
| |
| void FILE_close(FILE *p){ |
| if(p && p!=stdout && p!=stderr){ |
| fclose(p); |
| } |
| } |
| |
| void FILE_slurp(FILE *pFile, unsigned char **pOut, |
| unsigned * nOut){ |
| unsigned char zBuf[1024 * 8]; |
| unsigned char * pDest = 0; |
| unsigned nAlloc = 0; |
| unsigned nOff = 0; |
| /* Note that this needs to be able to work on non-seekable streams, |
| ** thus we read in chunks instead of doing a single alloc and |
| ** filling it in one go. */ |
| while( !feof(pFile) ){ |
| size_t const n = fread(zBuf, 1, sizeof(zBuf), pFile); |
| if(n>0){ |
| if(nAlloc < nOff + n + 1){ |
| nAlloc = nOff + n + 1; |
| pDest = cmpp_realloc(pDest, nAlloc); |
| } |
| memcpy(pDest + nOff, zBuf, n); |
| nOff += n; |
| } |
| } |
| if(pDest) pDest[nOff] = 0; |
| *pOut = pDest; |
| *nOut = nOff; |
| } |
| |
| void FileWrapper_close(FileWrapper * p){ |
| if(p->pFile) FILE_close(p->pFile); |
| if(p->zContent) cmpp_free(p->zContent); |
| *p = FileWrapper_empty; |
| } |
| |
| void FileWrapper_open(FileWrapper * p, const char * zName, |
| const char * zMode){ |
| FileWrapper_close(p); |
| p->pFile = FILE_open(zName, zMode); |
| p->zName = zName; |
| } |
| |
| void FileWrapper_slurp(FileWrapper * p){ |
| assert(!p->zContent); |
| assert(p->pFile); |
| FILE_slurp(p->pFile, &p->zContent, &p->nContent); |
| } |
| |
| unsigned count_lines(unsigned char const * zFrom, unsigned char const *zTo){ |
| unsigned ln = 0; |
| unsigned char const *zPos = zFrom; |
| assert(zFrom && zTo); |
| assert(zFrom <= zTo); |
| for(; zPos < zTo; ++zPos){ |
| switch(*zPos){ |
| case (unsigned)'\n': ++ln; break; |
| default: break; |
| } |
| } |
| return ln; |
| } |
| |
| enum CmppParseState { |
| TS_Start = 1, |
| TS_If, |
| TS_IfPassed, |
| TS_Else, |
| TS_Error |
| }; |
| typedef enum CmppParseState CmppParseState; |
| enum CmppTokenType { |
| TT_Invalid = 0, |
| TT_Comment, |
| TT_Define, |
| TT_Elif, |
| TT_ElifNot, |
| TT_Else, |
| TT_EndIf, |
| TT_Error, |
| TT_If, |
| TT_IfNot, |
| TT_Include, |
| TT_Line, |
| TT_Pragma, |
| TT_Stderr, |
| TT_Undef |
| }; |
| typedef enum CmppTokenType CmppTokenType; |
| |
| struct CmppToken { |
| CmppTokenType ttype; |
| /* Line number of this token in the source file. */ |
| unsigned lineNo; |
| /* Start of the token. */ |
| unsigned char const * zBegin; |
| /* One-past-the-end byte of the token. */ |
| unsigned char const * zEnd; |
| }; |
| typedef struct CmppToken CmppToken; |
| #define CmppToken_empty_m {TT_Invalid,0,0,0} |
| static const CmppToken CmppToken_empty = CmppToken_empty_m; |
| |
| /* |
| ** CmppLevel represents one "level" of tokenization, starting at the |
| ** top of the main input, incrementing once for each level of `#if`, |
| ** and decrementing for each `#endif`. |
| */ |
| typedef struct CmppLevel CmppLevel; |
| struct CmppLevel { |
| unsigned short flags; |
| /* |
| ** Used for controlling which parts of an if/elif/...endif chain |
| ** should get output. |
| */ |
| unsigned short skipLevel; |
| /* The token which started this level (an 'if' or 'ifnot'). */ |
| CmppToken token; |
| CmppParseState pstate; |
| }; |
| #define CmppLevel_empty_m {0U,0U,CmppToken_empty_m,TS_Start} |
| static const CmppLevel CmppLevel_empty = CmppLevel_empty_m; |
| enum CmppLevel_Flags { |
| /* Max depth of nested `#if` constructs in a single tokenizer. */ |
| CmppLevel_Max = 10, |
| /* Max number of keyword arguments. */ |
| CmppArgs_Max = 10, |
| /* Flag indicating that output for a CmpLevel should be elided. */ |
| CmppLevel_F_ELIDE = 0x01, |
| /* |
| ** Mask of CmppLevel::flags which are inherited when CmppLevel_push() |
| ** is used. |
| */ |
| CmppLevel_F_INHERIT_MASK = 0x01 |
| }; |
| |
| typedef struct CmppTokenizer CmppTokenizer; |
| typedef struct CmppKeyword CmppKeyword; |
| typedef void (*cmpp_keyword_f)(CmppKeyword const * pKw, CmppTokenizer * t); |
| struct CmppKeyword { |
| const char *zName; |
| unsigned nName; |
| int bTokenize; |
| CmppTokenType ttype; |
| cmpp_keyword_f xCall; |
| }; |
| |
| static CmppKeyword const * CmppKeyword_search(const char *zName); |
| static void cmpp_process_keyword(CmppTokenizer * const t); |
| |
| /* |
| ** Tokenizer for c-pp input files. |
| */ |
| struct CmppTokenizer { |
| const char * zName; /* Input (file) name for error reporting */ |
| unsigned const char * zBegin; /* start of input */ |
| unsigned const char * zEnd; /* one-after-the-end of input */ |
| unsigned const char * zAnchor; /* start of input or end point of |
| previous token */ |
| unsigned const char * zPos; /* current position */ |
| unsigned int lineNo; /* line # of current pos */ |
| CmppParseState pstate; |
| CmppToken token; /* current token result */ |
| struct { |
| unsigned ndx; |
| CmppLevel stack[CmppLevel_Max]; |
| } level; |
| /* Args for use in cmpp_keyword_f() impls. */ |
| struct { |
| CmppKeyword const * pKw; |
| int argc; |
| const unsigned char * argv[CmppArgs_Max]; |
| unsigned char lineBuf[1024]; |
| } args; |
| }; |
| #define CT_level(t) (t)->level.stack[(t)->level.ndx] |
| #define CT_pstate(t) CT_level(t).pstate |
| #define CT_skipLevel(t) CT_level(t).skipLevel |
| #define CLvl_skip(lvl) ((lvl)->skipLevel || ((lvl)->flags & CmppLevel_F_ELIDE)) |
| #define CT_skip(t) CLvl_skip(&CT_level(t)) |
| #define CmppTokenizer_empty_m { \ |
| 0,0,0,0,0,1U/*lineNo*/, \ |
| TS_Start, \ |
| CmppToken_empty_m, \ |
| {/*level*/0U,{CmppLevel_empty_m}}, \ |
| {/*args*/0,0,{0},{0}} \ |
| } |
| static const CmppTokenizer CmppTokenizer_empty = CmppTokenizer_empty_m; |
| |
| static void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n); |
| /*static void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...);*/ |
| |
| /* |
| ** Pushes a new level into the given tokenizer. Fails fatally if |
| ** it's too deep. |
| */ |
| static void CmppLevel_push(CmppTokenizer * const t); |
| /* |
| ** Pops a level from the tokenizer. Fails fatally if the top |
| ** level is popped. |
| */ |
| static void CmppLevel_pop(CmppTokenizer * const t); |
| /* |
| ** Returns the current level object. |
| */ |
| static CmppLevel * CmppLevel_get(CmppTokenizer * const t); |
| |
| /* |
| ** Global app state singleton. */ |
| static struct Global { |
| /* main()'s argv[0]. */ |
| const char * zArgv0; |
| /* |
| ** Bytes of the keyword delimiter/prefix. Owned |
| ** elsewhere. |
| */ |
| const char * zDelim; |
| /* Byte length of this->zDelim. */ |
| unsigned short nDelim; |
| /* If true, enables certain debugging output. */ |
| int doDebug; |
| /* App's db instance. */ |
| sqlite3 * db; |
| /* Output channel. */ |
| FileWrapper out; |
| struct { |
| sqlite3_stmt * defIns; |
| sqlite3_stmt * defDel; |
| sqlite3_stmt * defHas; |
| sqlite3_stmt * inclIns; |
| sqlite3_stmt * inclDel; |
| sqlite3_stmt * inclHas; |
| sqlite3_stmt * inclPathAdd; |
| sqlite3_stmt * inclSearch; |
| } stmt; |
| } g = { |
| "?", |
| CMPP_DEFAULT_DELIM/*zDelim*/, |
| (unsigned short) sizeof(CMPP_DEFAULT_DELIM)-1/*nDelim*/, |
| 0/*doDebug*/, |
| 0/*db*/, |
| FileWrapper_empty_m/*out*/, |
| {/*stmt*/ |
| 0/*defIns*/, 0/*defDel*/, 0/*defHas*/, |
| 0/*inclIns*/, 0/*inclDel*/, 0/*inclHas*/, |
| 0/*inclPathAdd*/ |
| } |
| }; |
| |
| |
| #if 0 |
| /* |
| ** Outputs a printf()-formatted message to c-pp's global output |
| ** channel. |
| */ |
| static void g_outf(char const *zFmt, ...); |
| void g_outf(char const *zFmt, ...){ |
| va_list va; |
| va_start(va, zFmt); |
| vfprintf(g.out.pFile, zFmt, va); |
| va_end(va); |
| } |
| #endif |
| |
| #if 0 |
| /* Outputs n bytes from z to c-pp's global output channel. */ |
| static void g_out(void const *z, unsigned int n); |
| void g_out(void const *z, unsigned int n){ |
| if(1!=fwrite(z, n, 1, g.out.pFile)){ |
| int const err = errno; |
| fatal("fwrite() output failed with errno #%d", err); |
| } |
| } |
| #endif |
| |
| void g_stderrv(char const *zFmt, va_list va){ |
| vfprintf(stderr, zFmt, va); |
| } |
| |
| void g_stderr(char const *zFmt, ...){ |
| va_list va; |
| va_start(va, zFmt); |
| g_stderrv(zFmt, va); |
| va_end(va); |
| } |
| |
| void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n){ |
| g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t))); |
| g_debug(3,("CT_skip() ?= %d\n",CT_skip(t))); |
| if(!CT_skip(t)){ |
| if(1!=fwrite(z, n, 1, g.out.pFile)){ |
| int const err = errno; |
| fatal("fwrite() output failed with errno #%d", err); |
| } |
| } |
| } |
| |
| void CmppLevel_push(CmppTokenizer * const t){ |
| CmppLevel * pPrev; |
| CmppLevel * p; |
| if(t->level.ndx+1 == (unsigned)CmppLevel_Max){ |
| fatal("%sif nesting level is too deep. Max=%d\n", |
| g.zDelim, CmppLevel_Max); |
| } |
| pPrev = &CT_level(t); |
| g_debug(3,("push from tokenizer level=%u flags=%04x\n", t->level.ndx, pPrev->flags)); |
| p = &t->level.stack[++t->level.ndx]; |
| *p = CmppLevel_empty; |
| p->token = t->token; |
| p->flags = (CmppLevel_F_INHERIT_MASK & pPrev->flags); |
| if(CLvl_skip(pPrev)) p->flags |= CmppLevel_F_ELIDE; |
| g_debug(3,("push to tokenizer level=%u flags=%04x\n", t->level.ndx, p->flags)); |
| } |
| |
| void CmppLevel_pop(CmppTokenizer * const t){ |
| if(!t->level.ndx){ |
| fatal("Internal error: CmppLevel_pop() at the top of the stack"); |
| } |
| g_debug(3,("pop from tokenizer level=%u, flags=%04x skipLevel?=%d\n", t->level.ndx, |
| t->level.stack[t->level.ndx].flags, CT_skipLevel(t))); |
| g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t))); |
| g_debug(3,("CT_skip() ?= %d\n",CT_skip(t))); |
| t->level.stack[t->level.ndx--] = CmppLevel_empty; |
| g_debug(3,("pop to tokenizer level=%u, flags=%04x\n", t->level.ndx, |
| t->level.stack[t->level.ndx].flags)); |
| g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t))); |
| g_debug(3,("CT_skip() ?= %d\n",CT_skip(t))); |
| } |
| |
| CmppLevel * CmppLevel_get(CmppTokenizer * const t){ |
| return &t->level.stack[t->level.ndx]; |
| } |
| |
| |
| void db_affirm_rc(int rc, const char * zMsg){ |
| if(rc){ |
| fatal("Db error #%d %s: %s", rc, zMsg, sqlite3_errmsg(g.db)); |
| } |
| } |
| |
| void db_finalize(sqlite3_stmt *pStmt){ |
| sqlite3_finalize(pStmt); |
| } |
| |
| int db_step(sqlite3_stmt *pStmt){ |
| int const rc = sqlite3_step(pStmt); |
| if(SQLITE_ROW!=rc && SQLITE_DONE!=rc){ |
| db_affirm_rc(rc, "from db_step()"); |
| } |
| return rc; |
| } |
| |
| static sqlite3_str * db_str_new(void){ |
| sqlite3_str * rc = sqlite3_str_new(g.db); |
| if(!rc) fatal("Alloc failed for sqlite3_str_new()"); |
| return rc; |
| } |
| |
| static char * db_str_finish(sqlite3_str *s, int * n){ |
| int const rc = sqlite3_str_errcode(s); |
| if(rc) fatal("Error #%d from sqlite3_str_errcode()", rc); |
| if(n) *n = sqlite3_str_length(s); |
| char * z = sqlite3_str_finish(s); |
| if(!z) fatal("Alloc failed for sqlite3_str_new()"); |
| return z; |
| } |
| |
| void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...){ |
| int rc; |
| sqlite3_str * str = db_str_new(); |
| char * z = 0; |
| int n = 0; |
| va_list va; |
| if(!str) fatal("sqlite3_str_new() failed"); |
| va_start(va, zSql); |
| sqlite3_str_vappendf(str, zSql, va); |
| va_end(va); |
| rc = sqlite3_str_errcode(str); |
| if(rc) fatal("sqlite3_str_errcode() = %d", rc); |
| z = db_str_finish(str, &n); |
| rc = sqlite3_prepare_v2(g.db, z, n, pStmt, 0); |
| if(rc) fatal("Error #%d (%s) preparing: %s", |
| rc, sqlite3_errmsg(g.db), z); |
| sqlite3_free(z); |
| } |
| |
| void db_bind_int(sqlite3_stmt *pStmt, int col, int val){ |
| int const rc = sqlite3_bind_int(pStmt, col, val); |
| db_affirm_rc(rc,"from db_bind_int()"); |
| } |
| |
| #if 0 |
| void db_bind_null(sqlite3_stmt *pStmt, int col){ |
| int const rc = sqlite3_bind_null(pStmt, col); |
| db_affirm_rc(rc,"from db_bind_null()"); |
| } |
| #endif |
| |
| void db_bind_textn(sqlite3_stmt *pStmt, int col, |
| const char * zStr, int n){ |
| int const rc = zStr |
| ? sqlite3_bind_text(pStmt, col, zStr, n, SQLITE_TRANSIENT) |
| : sqlite3_bind_null(pStmt, col); |
| db_affirm_rc(rc,"from db_bind_textn()"); |
| } |
| |
| void db_bind_text(sqlite3_stmt *pStmt, int col, |
| const char * zStr){ |
| db_bind_textn(pStmt, col, zStr, -1); |
| } |
| |
| #if 0 |
| void db_bind_textv(sqlite3_stmt *pStmt, int col, |
| const char * zFmt, ...){ |
| int rc; |
| sqlite3_str * str = db_str_new(); |
| int n = 0; |
| char * z; |
| va_list va; |
| va_start(va,zFmt); |
| sqlite3_str_vappendf(str, zFmt, va); |
| va_end(va); |
| z = db_str_finish(str, &n); |
| rc = sqlite3_bind_text(pStmt, col, z, n, sqlite3_free); |
| db_affirm_rc(rc,"from db_bind_textv()"); |
| } |
| #endif |
| |
| void db_free(void *m){ |
| sqlite3_free(m); |
| } |
| |
| void db_define_add(const char * zKey){ |
| int rc; |
| if(!g.stmt.defIns){ |
| db_prepare(&g.stmt.defIns, |
| "INSERT OR REPLACE INTO def(k) VALUES(?)"); |
| } |
| db_bind_text(g.stmt.defIns, 1, zKey); |
| rc = db_step(g.stmt.defIns); |
| if(SQLITE_DONE != rc){ |
| db_affirm_rc(rc, "Stepping INSERT on def"); |
| } |
| g_debug(2,("define: %s\n",zKey)); |
| sqlite3_reset(g.stmt.defIns); |
| } |
| |
| int db_define_has(const char * zName){ |
| int rc; |
| if(!g.stmt.defHas){ |
| db_prepare(&g.stmt.defHas, "SELECT 1 FROM def WHERE k=?"); |
| } |
| db_bind_text(g.stmt.defHas, 1, zName); |
| rc = db_step(g.stmt.defHas); |
| if(SQLITE_ROW == rc){ |
| rc = 1; |
| }else{ |
| assert(SQLITE_DONE==rc); |
| rc = 0; |
| } |
| g_debug(1,("defined [%s] ?= %d\n",zName, rc)); |
| sqlite3_clear_bindings(g.stmt.defHas); |
| sqlite3_reset(g.stmt.defHas); |
| return rc; |
| } |
| |
| |
| void db_define_rm(const char * zKey){ |
| int rc; |
| int n = 0; |
| const char *zPos = zKey; |
| if(!g.stmt.defDel){ |
| db_prepare(&g.stmt.defDel, "DELETE FROM def WHERE k=?"); |
| } |
| for( ; *zPos && '='!=*zPos; ++n, ++zPos) {} |
| db_bind_text(g.stmt.defDel, 1, zKey); |
| rc = db_step(g.stmt.defDel); |
| if(SQLITE_DONE != rc){ |
| db_affirm_rc(rc, "Stepping DELETE on def"); |
| } |
| g_debug(2,("undefine: %.*s\n",n, zKey)); |
| sqlite3_clear_bindings(g.stmt.defDel); |
| sqlite3_reset(g.stmt.defDel); |
| } |
| |
| void db_including_add(const char * zKey, const char * zSrc, int srcLine){ |
| int rc; |
| if(!g.stmt.inclIns){ |
| db_prepare(&g.stmt.inclIns, |
| "INSERT OR FAIL INTO incl(file,srcFile,srcLine) VALUES(?,?,?)"); |
| } |
| db_bind_text(g.stmt.inclIns, 1, zKey); |
| db_bind_text(g.stmt.inclIns, 2, zSrc); |
| db_bind_int(g.stmt.inclIns, 3, srcLine); |
| rc = db_step(g.stmt.inclIns); |
| if(SQLITE_DONE != rc){ |
| db_affirm_rc(rc, "Stepping INSERT on incl"); |
| } |
| g_debug(2,("inclpath add [%s] from [%s]:%d\n", zKey, zSrc, srcLine)); |
| sqlite3_clear_bindings(g.stmt.inclIns); |
| sqlite3_reset(g.stmt.inclIns); |
| } |
| |
| void db_include_rm(const char * zKey){ |
| int rc; |
| if(!g.stmt.inclDel){ |
| db_prepare(&g.stmt.inclDel, "DELETE FROM incl WHERE file=?"); |
| } |
| db_bind_text(g.stmt.inclDel, 1, zKey); |
| rc = db_step(g.stmt.inclDel); |
| if(SQLITE_DONE != rc){ |
| db_affirm_rc(rc, "Stepping DELETE on incl"); |
| } |
| g_debug(2,("inclpath rm [%s]\n", zKey)); |
| sqlite3_clear_bindings(g.stmt.inclDel); |
| sqlite3_reset(g.stmt.inclDel); |
| } |
| |
| char * db_include_search(const char * zKey){ |
| char * zName = 0; |
| if(!g.stmt.inclSearch){ |
| db_prepare(&g.stmt.inclSearch, |
| "SELECT ?1 fn WHERE fileExists(fn) " |
| "UNION ALL SELECT * FROM (" |
| "SELECT replace(dir||'/'||?1, '//','/') AS fn " |
| "FROM inclpath WHERE fileExists(fn) ORDER BY seq" |
| ")"); |
| } |
| db_bind_text(g.stmt.inclSearch, 1, zKey); |
| if(SQLITE_ROW==db_step(g.stmt.inclSearch)){ |
| const unsigned char * z = sqlite3_column_text(g.stmt.inclSearch, 0); |
| zName = z ? sqlite3_mprintf("%s", z) : 0; |
| if(!zName) fatal("Alloc failed"); |
| } |
| sqlite3_clear_bindings(g.stmt.inclSearch); |
| sqlite3_reset(g.stmt.inclSearch); |
| return zName; |
| } |
| |
| static int db_including_has(const char * zName){ |
| int rc; |
| if(!g.stmt.inclHas){ |
| db_prepare(&g.stmt.inclHas, "SELECT 1 FROM incl WHERE file=?"); |
| } |
| db_bind_text(g.stmt.inclHas, 1, zName); |
| rc = db_step(g.stmt.inclHas); |
| if(SQLITE_ROW == rc){ |
| rc = 1; |
| }else{ |
| assert(SQLITE_DONE==rc); |
| rc = 0; |
| } |
| g_debug(2,("inclpath has [%s] = %d\n",zName, rc)); |
| sqlite3_clear_bindings(g.stmt.inclHas); |
| sqlite3_reset(g.stmt.inclHas); |
| return rc; |
| } |
| |
| #if 0 |
| /* |
| ** Fails fatally if the `#include` list contains the given key. |
| */ |
| static void db_including_check(const char * zKey); |
| void db_including_check(const char * zName){ |
| if(db_including_has(zName)){ |
| fatal("Recursive include detected: %s\n", zName); |
| } |
| } |
| #endif |
| |
| void db_include_dir_add(const char * zDir){ |
| static int seq = 0; |
| int rc; |
| if(!g.stmt.inclPathAdd){ |
| db_prepare(&g.stmt.inclPathAdd, |
| "INSERT OR FAIL INTO inclpath(seq,dir) VALUES(?,?)"); |
| } |
| db_bind_int(g.stmt.inclPathAdd, 1, ++seq); |
| db_bind_text(g.stmt.inclPathAdd, 2, zDir); |
| rc = db_step(g.stmt.inclPathAdd); |
| if(SQLITE_DONE != rc){ |
| db_affirm_rc(rc, "Stepping INSERT on inclpath"); |
| } |
| g_debug(2,("inclpath add #%d: %s\n",seq, zDir)); |
| sqlite3_clear_bindings(g.stmt.inclPathAdd); |
| sqlite3_reset(g.stmt.inclPathAdd); |
| } |
| |
| static void cmpp_atexit(void){ |
| #define FINI(M) if(g.stmt.M) sqlite3_finalize(g.stmt.M) |
| FINI(defIns); FINI(defDel); FINI(defHas); |
| FINI(inclIns); FINI(inclDel); FINI(inclHas); |
| FINI(inclPathAdd); FINI(inclSearch); |
| #undef FINI |
| FileWrapper_close(&g.out); |
| if(g.db) sqlite3_close(g.db); |
| } |
| |
| /* |
| ** sqlite3 UDF which returns true if its argument refers to an |
| ** accessible file, else false. |
| */ |
| static void udf_file_exists( |
| sqlite3_context *context, |
| int argc, |
| sqlite3_value **argv |
| ){ |
| const char *zName; |
| (void)(argc); /* Unused parameter */ |
| zName = (const char*)sqlite3_value_text(argv[0]); |
| if( zName==0 ) return; |
| sqlite3_result_int(context, 0==access(zName, 0)); |
| } |
| |
| /* Initialize g.db, failing fatally on error. */ |
| static void cmpp_initdb(void){ |
| int rc; |
| char * zErr = 0; |
| const char * zSchema = |
| "CREATE TABLE def(" |
| "k TEXT PRIMARY KEY NOT NULL" |
| /*"v INTEGER DEFAULT 1"*/ |
| ") WITHOUT ROWID;" |
| /* ^^^ defines */ |
| "CREATE TABLE incl(" |
| "file TEXT PRIMARY KEY NOT NULL," |
| "srcFile TEXT DEFAULT NULL," |
| "srcLine INTEGER DEFAULT 0" |
| ") WITHOUT ROWID;" |
| /* ^^^ files currently being included */ |
| "CREATE TABLE inclpath(" |
| "seq INTEGER UNIQUE, " |
| "dir TEXT PRIMARY KEY NOT NULL ON CONFLICT IGNORE" |
| ")" |
| /* ^^^ include path */ |
| ; |
| assert(0==g.db); |
| if(g.db) return; |
| rc = sqlite3_open_v2(":memory:", &g.db, SQLITE_OPEN_READWRITE, 0); |
| if(rc) fatal("Error opening :memory: db."); |
| rc = sqlite3_exec(g.db, zSchema, 0, 0, &zErr); |
| if(rc) fatal("Error initializing database: %s", zErr); |
| rc = sqlite3_create_function(g.db, "fileExists", 1, |
| SQLITE_UTF8|SQLITE_DIRECTONLY, 0, |
| udf_file_exists, 0, 0); |
| db_affirm_rc(rc, "UDF registration failed."); |
| } |
| |
| /* |
| ** For position zPos, which must be in the half-open range |
| ** [zBegin,zEnd), returns g.nDelim if it is at the start of a line and |
| ** starts with g.zDelim, else returns 0. |
| */ |
| static unsigned short cmpp_is_delim(unsigned char const *zBegin, |
| unsigned char const *zEnd, |
| unsigned char const *zPos){ |
| assert(zEnd>zBegin); |
| assert(zPos<zEnd); |
| assert(zPos>=zBegin); |
| if(zPos>zBegin && |
| ('\n'!=*(zPos - 1) |
| || ((unsigned)(zEnd - zPos) <= g.nDelim))){ |
| return 0; |
| }else if(0==memcmp(zPos, g.zDelim, g.nDelim)){ |
| return g.nDelim; |
| }else{ |
| return 0; |
| } |
| } |
| |
| /* |
| ** Scans t to the next keyword line, emitting all input before that |
| ** which is _not_ a keyword line unless it's elided due to being |
| ** inside a block which elides its content. Returns 0 if no keyword |
| ** line was found, in which case the end of the input has been |
| ** reached, else returns a truthy value and sets up t's state for use |
| ** with cmpp_process_keyword(), which should then be called. |
| */ |
| static int cmpp_next_keyword_line(CmppTokenizer * const t){ |
| unsigned char const * zStart; |
| unsigned char const * z; |
| CmppToken * const tok = &t->token; |
| unsigned short isDelim = 0; |
| |
| assert(t->zBegin); |
| assert(t->zEnd > t->zBegin); |
| if(!t->zPos) t->zPos = t->zBegin; |
| t->zAnchor = t->zPos; |
| zStart = z = t->zPos; |
| *tok = CmppToken_empty; |
| while(z<t->zEnd |
| && 0==(isDelim = cmpp_is_delim(t->zBegin, t->zEnd, z))){ |
| ++z; |
| } |
| if(z>zStart){ |
| /* We passed up content */ |
| cmpp_t_out(t, zStart, (unsigned)(z - zStart)); |
| } |
| assert(isDelim==0 || isDelim==g.nDelim); |
| tok->lineNo = t->lineNo += count_lines(zStart, z); |
| if(isDelim){ |
| /* Handle backslash-escaped newlines */ |
| int isEsc = 0, atEol = 0; |
| tok->zBegin = z+isDelim; |
| for( ++z ; z<t->zEnd && 0==atEol; ++z ){ |
| switch((int)*z){ |
| case (int)'\\': |
| isEsc = 0==isEsc; break; |
| case (int)'\n': |
| atEol = 0==isEsc; |
| isEsc = 0; |
| ++t->lineNo; |
| break; |
| default: |
| break; |
| } |
| } |
| tok->zEnd = atEol ? z-1 : z; |
| /* Strip leading spaces */ |
| while(tok->zBegin < tok->zEnd && isspace((char)(*tok->zBegin))){ |
| ++tok->zBegin; |
| } |
| tok->ttype = TT_Line; |
| g_debug(2,("Keyword @ line %u: [[[%.*s]]]\n", |
| tok->lineNo, |
| (int)(tok->zEnd-tok->zBegin), tok->zBegin)); |
| } |
| t->zPos = z; |
| if(isDelim){ |
| /* Split t->token into arguments for the line's keyword */ |
| int i, argc = 0, prevChar = 0; |
| const unsigned tokLen = (unsigned)(tok->zEnd - tok->zBegin); |
| unsigned char * zKwd; |
| unsigned char * zEsc; |
| unsigned char * zz; |
| |
| assert(TT_Line==tok->ttype); |
| if((unsigned)sizeof(t->args.lineBuf) < tokLen + 1){ |
| fatal("Keyword line is unreasonably long: %.*s", |
| tokLen, tok->zBegin); |
| }else if(!tokLen){ |
| fatal("Line #%u has no keyword after delimiter", tok->lineNo); |
| } |
| g_debug(2,("token @ line %u len=%u [[[%.*s]]]\n", |
| tok->lineNo, tokLen, tokLen, tok->zBegin)); |
| zKwd = &t->args.lineBuf[0]; |
| memcpy(zKwd, tok->zBegin, tokLen); |
| memset(zKwd + tokLen, 0, sizeof(t->args.lineBuf) - tokLen); |
| for( zEsc = 0, zz = zKwd; *zz; ++zz ){ |
| /* Convert backslash-escaped newlines to whitespace */ |
| switch((int)*zz){ |
| case (int)'\\': |
| if(zEsc) zEsc = 0; |
| else zEsc = zz; |
| break; |
| case (int)'\n': |
| assert(zEsc && "Should not have an unescaped newline?"); |
| if(zEsc==zz-1){ |
| *zEsc = (unsigned char)' '; |
| /* FIXME?: memmove() lnBuf content one byte to the left here |
| ** to collapse backslash and newline into a single |
| ** byte. Also consider collapsing all leading space on the |
| ** next line. */ |
| } |
| zEsc = 0; |
| *zz = (unsigned char)' '; |
| break; |
| default: |
| zEsc = 0; |
| break; |
| } |
| } |
| t->args.argv[argc++] = zKwd; |
| for( zz = zKwd; *zz; ++zz ){ |
| if(isspace(*zz)){ |
| *zz = 0; |
| break; |
| } |
| } |
| t->args.pKw = CmppKeyword_search((char const *)zKwd); |
| if(!t->args.pKw){ |
| fatal("Unknown keyword '%s' at line %u\n", (char const *)zKwd, |
| tok->lineNo); |
| } |
| for( ++zz ; *zz && isspace(*zz); ++zz ){} |
| if(t->args.pKw->bTokenize){ |
| for( ; *zz; prevChar = *zz, ++zz ){ |
| /* Split string into word-shaped tokens. |
| ** TODO ?= quoted strings, for the sake of the |
| ** #error keyword. */ |
| if(isspace(*zz)){ |
| assert(zz!=zKwd && "Leading space was stripped earlier."); |
| *zz = 0; |
| }else{ |
| if(argc == (int)CmppArgs_Max){ |
| fatal("Too many arguments @ line %u: %.*s", |
| tok->lineNo, tokLen, tok->zBegin); |
| }else if(zz>zKwd && !prevChar){ |
| t->args.argv[argc++] = zz; |
| } |
| } |
| } |
| }else{ |
| /* Treat rest of line as one token */ |
| if(*zz) t->args.argv[argc++] = zz; |
| } |
| tok->ttype = t->args.pKw->ttype; |
| if(g.doDebug>1){ |
| for(i = 0; i < argc; ++i){ |
| g_debug(0,("line %u arg #%d=%s\n", |
| tok->lineNo, i, |
| (char const *)t->args.argv[i])); |
| } |
| } |
| t->args.argc = argc; |
| }else{ |
| t->args.pKw = 0; |
| t->args.argc = 0; |
| } |
| return isDelim; |
| } |
| |
| static void cmpp_kwd__err_prefix(CmppKeyword const * pKw, CmppTokenizer *t, |
| char const *zPrefix){ |
| g_stderr("%s%s%s @ %s line %u: ", |
| zPrefix ? zPrefix : "", |
| zPrefix ? ": " : "", |
| pKw->zName, t->zName, t->token.lineNo); |
| } |
| |
| /* Internal error reporting helper for cmpp_keyword_f() impls. */ |
| static CMPP_NORETURN void cmpp_kwd__misuse(CmppKeyword const * pKw, |
| CmppTokenizer *t, |
| char const *zFmt, ...){ |
| va_list va; |
| cmpp_kwd__err_prefix(pKw, t, "Fatal error"); |
| va_start(va, zFmt); |
| fatalv(zFmt, va); |
| va_end(va); |
| } |
| |
| /* No-op cmpp_keyword_f() impl. */ |
| static void cmpp_kwd_noop(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(t || pKw){/*unused*/} |
| } |
| |
| /* #error impl. */ |
| static void cmpp_kwd_error(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(CT_skip(t)) return; |
| else{ |
| assert(t->args.argc < 3); |
| const char *zBegin = t->args.argc>1 |
| ? (const char *)t->args.argv[1] : 0; |
| cmpp_kwd__err_prefix(pKw, t, NULL); |
| fatal("%s", zBegin ? zBegin : "(no additional info)"); |
| } |
| } |
| |
| /* Impl. for #define, #undef */ |
| static void cmpp_kwd_define(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(CT_skip(t)) return; |
| if(t->args.argc<2){ |
| cmpp_kwd__misuse(pKw, t, "Expecting one or more arguments"); |
| }else{ |
| int i = 1; |
| void (*func)(const char *) = TT_Define==pKw->ttype |
| ? db_define_add : db_define_rm; |
| for( ; i < t->args.argc; ++i){ |
| func( (char const *)t->args.argv[i] ); |
| } |
| } |
| } |
| |
| /* Impl. for #if, #ifnot, #elif, #elifnot. */ |
| static void cmpp_kwd_if(CmppKeyword const * pKw, CmppTokenizer *t){ |
| int buul; |
| CmppParseState tmpState = TS_Start; |
| if(t->args.argc!=2){ |
| cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 argument"); |
| } |
| /*g_debug(0,("%s %s level %u pstate=%d\n", pKw->zName, |
| (char const *)t->args.argv[1], |
| t->level.ndx, (int)CT_pstate(t)));*/ |
| switch(pKw->ttype){ |
| case TT_Elif: |
| case TT_ElifNot: |
| switch(CT_pstate(t)){ |
| case TS_If: break; |
| case TS_IfPassed: CT_level(t).flags |= CmppLevel_F_ELIDE; return; |
| default: goto misuse; |
| } |
| break; |
| case TT_If: |
| case TT_IfNot: |
| CmppLevel_push(t); |
| break; |
| default: |
| cmpp_kwd__misuse(pKw, t, "Unpexected keyword token type"); |
| break; |
| } |
| buul = db_define_has((char const *)t->args.argv[1]); |
| if(TT_IfNot==pKw->ttype || TT_ElifNot==pKw->ttype) buul = !buul; |
| if(buul){ |
| CT_pstate(t) = tmpState = TS_IfPassed; |
| CT_skipLevel(t) = 0; |
| }else{ |
| CT_pstate(t) = TS_If /* also for TT_IfNot, TT_Elif, TT_ElifNot */; |
| CT_skipLevel(t) = 1; |
| g_debug(3,("setting CT_skipLevel = 1 @ level %d\n", t->level.ndx)); |
| } |
| if(TT_If==pKw->ttype || TT_IfNot==pKw->ttype){ |
| unsigned const lvlIf = t->level.ndx; |
| CmppToken const lvlToken = CT_level(t).token; |
| while(cmpp_next_keyword_line(t)){ |
| cmpp_process_keyword(t); |
| if(lvlIf > t->level.ndx){ |
| assert(TT_EndIf == t->token.ttype); |
| break; |
| } |
| #if 0 |
| if(TS_IfPassed==tmpState){ |
| tmpState = TS_Start; |
| t->level.stack[lvlIf].flags |= CmppLevel_F_ELIDE; |
| g_debug(1,("Setting ELIDE for TS_IfPassed @ lv %d (lvlIf=%d)\n", t->level.ndx, lvlIf)); |
| } |
| #endif |
| } |
| if(lvlIf <= t->level.ndx){ |
| cmpp_kwd__err_prefix(pKw, t, NULL); |
| fatal("Input ended inside an unterminated %sif " |
| "opened at [%s] line %u", |
| g.zDelim, t->zName, lvlToken.lineNo); |
| } |
| } |
| return; |
| misuse: |
| cmpp_kwd__misuse(pKw, t, "'%s' used out of context", |
| pKw->zName); |
| } |
| |
| /* Impl. for #else. */ |
| static void cmpp_kwd_else(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(t->args.argc>1){ |
| cmpp_kwd__misuse(pKw, t, "Expecting no arguments"); |
| } |
| switch(CT_pstate(t)){ |
| case TS_IfPassed: CT_skipLevel(t) = 1; break; |
| case TS_If: CT_skipLevel(t) = 0; break; |
| default: |
| cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'", |
| pKw->zName); |
| } |
| /*g_debug(0,("else flags=0x%02x skipLevel=%u\n", |
| CT_level(t).flags, CT_level(t).skipLevel));*/ |
| CT_pstate(t) = TS_Else; |
| } |
| |
| /* Impl. for #endif. */ |
| static void cmpp_kwd_endif(CmppKeyword const * pKw, CmppTokenizer *t){ |
| /* Maintenance reminder: we ignore all arguments after the endif |
| ** to allow for constructs like: |
| ** |
| ** #endif // foo |
| ** |
| ** in a manner which does not require a specific comment style */ |
| switch(CT_pstate(t)){ |
| case TS_Else: |
| case TS_If: |
| case TS_IfPassed: |
| break; |
| default: |
| cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'", |
| pKw->zName); |
| } |
| CmppLevel_pop(t); |
| } |
| |
| /* Impl. for #include. */ |
| static void cmpp_kwd_include(CmppKeyword const * pKw, CmppTokenizer *t){ |
| char const * zFile; |
| char * zResolved; |
| if(CT_skip(t)) return; |
| else if(t->args.argc!=2){ |
| cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 filename argument"); |
| } |
| zFile = (const char *)t->args.argv[1]; |
| if(db_including_has(zFile)){ |
| /* Note that different spellings of the same filename |
| ** will elude this check, but that seems okay, as different |
| ** spellings means that we're not re-running the exact same |
| ** invocation. We might want some other form of multi-include |
| ** protection, rather than this, however. There may well be |
| ** sensible uses for recursion. */ |
| cmpp_kwd__err_prefix(pKw, t, NULL); |
| fatal("Recursive include of file: %s", zFile); |
| } |
| zResolved = db_include_search(zFile); |
| if(zResolved){ |
| db_including_add(zFile, t->zName, t->token.lineNo); |
| cmpp_process_file(zResolved); |
| db_include_rm(zFile); |
| db_free(zResolved); |
| }else{ |
| cmpp_kwd__err_prefix(pKw, t, NULL); |
| fatal("file not found: %s", zFile); |
| } |
| } |
| |
| /* Impl. for #pragma. */ |
| static void cmpp_kwd_pragma(CmppKeyword const * pKw, CmppTokenizer *t){ |
| const char * zArg; |
| if(CT_skip(t)) return; |
| else if(t->args.argc!=2){ |
| cmpp_kwd__misuse(pKw, t, "Expecting one argument"); |
| } |
| zArg = (const char *)t->args.argv[1]; |
| #define M(X) 0==strcmp(zArg,X) |
| if(M("defines")){ |
| sqlite3_stmt * q = 0; |
| db_prepare(&q, "SELECT k FROM def ORDER BY k"); |
| g_stderr("cmpp defines:\n"); |
| while(SQLITE_ROW==db_step(q)){ |
| int const n = sqlite3_column_bytes(q, 0); |
| const char * z = (const char *)sqlite3_column_text(q, 0); |
| g_stderr("\t%.*s\n", n, z); |
| } |
| db_finalize(q); |
| }else{ |
| cmpp_kwd__misuse(pKw, t, "Unknown pragma"); |
| } |
| #undef M |
| } |
| |
| /* #stder impl. */ |
| static void cmpp_kwd_stderr(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(CT_skip(t)) return; |
| else{ |
| const char *zBegin = t->args.argc>1 |
| ? (const char *)t->args.argv[1] : 0; |
| if(zBegin){ |
| g_stderr("%s:%u: %s\n", t->zName, t->token.lineNo, zBegin); |
| }else{ |
| g_stderr("%s:%u: (no %.*s%s argument)\n", |
| t->zName, t->token.lineNo, |
| g.nDelim, g.zDelim, pKw->zName); |
| } |
| } |
| } |
| |
| #if 0 |
| /* Impl. for dummy placeholder. */ |
| static void cmpp_kwd_todo(CmppKeyword const * pKw, CmppTokenizer *t){ |
| if(t){/*unused*/} |
| g_debug(0,("TODO: keyword handler for %s\n", pKw->zName)); |
| } |
| #endif |
| |
| CmppKeyword aKeywords[] = { |
| /* Keep these sorted by zName */ |
| {"//", 2, 0, TT_Comment, cmpp_kwd_noop}, |
| {"define", 6, 1, TT_Define, cmpp_kwd_define}, |
| {"elif", 4, 1, TT_Elif, cmpp_kwd_if}, |
| {"elifnot", 7, 1, TT_ElifNot, cmpp_kwd_if}, |
| {"else", 4, 1, TT_Else, cmpp_kwd_else}, |
| {"endif", 5, 0, TT_EndIf, cmpp_kwd_endif}, |
| {"error", 4, 0, TT_Error, cmpp_kwd_error}, |
| {"if", 2, 1, TT_If, cmpp_kwd_if}, |
| {"ifnot", 5, 1, TT_IfNot, cmpp_kwd_if}, |
| {"include", 7, 0, TT_Include, cmpp_kwd_include}, |
| {"pragma", 6, 1, TT_Pragma, cmpp_kwd_pragma}, |
| {"stderr", 6, 0, TT_Stderr, cmpp_kwd_stderr}, |
| {"undef", 5, 1, TT_Undef, cmpp_kwd_define}, |
| {0,0,TT_Invalid, 0} |
| }; |
| |
| static int cmp_CmppKeyword(const void *p1, const void *p2){ |
| char const * zName = (const char *)p1; |
| CmppKeyword const * kw = (CmppKeyword const *)p2; |
| return strcmp(zName, kw->zName); |
| } |
| |
| CmppKeyword const * CmppKeyword_search(const char *zName){ |
| return (CmppKeyword const *)bsearch(zName, &aKeywords[0], |
| sizeof(aKeywords)/sizeof(aKeywords[0]) - 1, |
| sizeof(aKeywords[0]), |
| cmp_CmppKeyword); |
| } |
| |
| void cmpp_process_keyword(CmppTokenizer * const t){ |
| assert(t->args.pKw); |
| assert(t->args.argc); |
| t->args.pKw->xCall(t->args.pKw, t); |
| t->args.pKw = 0; |
| t->args.argc = 0; |
| } |
| |
| void cmpp_process_file(const char * zName){ |
| FileWrapper fw = FileWrapper_empty; |
| CmppTokenizer ct = CmppTokenizer_empty; |
| |
| FileWrapper_open(&fw, zName, "r"); |
| FileWrapper_slurp(&fw); |
| g_debug(1,("Read %u byte(s) from [%s]\n", fw.nContent, fw.zName)); |
| ct.zName = zName; |
| ct.zBegin = fw.zContent; |
| ct.zEnd = fw.zContent + fw.nContent; |
| while(cmpp_next_keyword_line(&ct)){ |
| cmpp_process_keyword(&ct); |
| } |
| FileWrapper_close(&fw); |
| if(0!=ct.level.ndx){ |
| CmppLevel * const lv = CmppLevel_get(&ct); |
| fatal("Input ended inside an unterminated nested construct" |
| "opened at [%s] line %u", zName, lv->token.lineNo); |
| } |
| } |
| |
| static void usage(int isErr){ |
| FILE * const fOut = isErr ? stderr : stdout; |
| fprintf(fOut, |
| "Usage: %s [flags] [infile]\n" |
| "Flags:\n", |
| g.zArgv0); |
| #define arg(F,D) fprintf(fOut," %s\n %s\n",F, D) |
| arg("-f|--file FILE","Read input from FILE (default=- (stdin)).\n" |
| " Alternately, the first non-flag argument is assumed to " |
| "be the input file."); |
| arg("-o|--outfile FILE","Send output to FILE (default=- (stdout))"); |
| arg("-DXYZ","Define XYZ to true"); |
| arg("-UXYZ","Undefine XYZ (equivalent to false)"); |
| arg("-IXYZ","Add dir XYZ to include path"); |
| arg("-d|--delimiter VALUE", "Set keyword delimiter to VALUE " |
| "(default=" CMPP_DEFAULT_DELIM ")"); |
| #undef arg |
| fputs("",fOut); |
| } |
| |
| int main(int argc, char const * const * argv){ |
| int rc = 0; |
| int i; |
| int inclCount = 0; |
| const char * zInfile = 0; |
| #define M(X) (0==strcmp(X,zArg)) |
| #define ISFLAG(X) else if(M(X)) |
| #define ISFLAG2(X,Y) else if(M(X) || M(Y)) |
| #define ARGVAL \ |
| if(i+1>=argc) fatal("Missing value for flag '%s'", zArg); \ |
| zArg = argv[++i] |
| g.zArgv0 = argv[0]; |
| atexit(cmpp_atexit); |
| cmpp_initdb(); |
| for(i = 1; i < argc; ++i){ |
| char const * zArg = argv[i]; |
| while('-'==*zArg) ++zArg; |
| if(M("?") || M("help")) { |
| usage(0); |
| goto end; |
| }else if('D'==*zArg){ |
| ++zArg; |
| if(!*zArg) fatal("Missing key for -D"); |
| db_define_add(zArg); |
| }else if('U'==*zArg){ |
| ++zArg; |
| if(!*zArg) fatal("Missing key for -U"); |
| db_define_rm(zArg); |
| }else if('I'==*zArg){ |
| ++zArg; |
| if(!*zArg) fatal("Missing directory for -I"); |
| db_include_dir_add(zArg); |
| ++inclCount; |
| } |
| ISFLAG2("o","outfile"){ |
| ARGVAL; |
| if(g.out.zName) fatal("Cannot use -o more than once."); |
| g.out.zName = zArg; |
| } |
| ISFLAG2("f","file"){ |
| ARGVAL; |
| do_infile: |
| if(zInfile) fatal("Cannot use -i more than once."); |
| zInfile = zArg; |
| } |
| ISFLAG2("d","delimiter"){ |
| ARGVAL; |
| g.zDelim = zArg; |
| g.nDelim = (unsigned short)strlen(zArg); |
| if(!g.nDelim) fatal("Keyword delimiter may not be empty."); |
| } |
| ISFLAG("debug"){ |
| ++g.doDebug; |
| }else if(!zInfile && '-'!=argv[i][0]){ |
| goto do_infile; |
| }else{ |
| fatal("Unhandled flag: %s", argv[i]); |
| } |
| } |
| if(!zInfile) zInfile = "-"; |
| if(!g.out.zName) g.out.zName = "-"; |
| if(!inclCount) db_include_dir_add("."); |
| FileWrapper_open(&g.out, g.out.zName, "w"); |
| cmpp_process_file(zInfile); |
| FileWrapper_close(&g.out); |
| end: |
| return rc ? EXIT_FAILURE : EXIT_SUCCESS; |
| } |
| |
| #undef CT_level |
| #undef CT_pstate |
| #undef CT_skipLevel |
| #undef CT_skip |
| #undef CLvl_skip |